feat: Complete ApsGo hydroponics system with multiple features
Features implemented: - Dashboard with real-time sensor monitoring (temp, humidity, light, soil moisture) - Water flow status indicator with color coding - Time-based scheduling (Kontrol Waktu/Jadwal) - Sensor-based threshold control (Kontrol Sensor/Threshold) - Smart mode with auto-stop when target reached - Three pump controls: Air, Larutan Nutrisi, Pengaduk - Manual control for all actuators - History/logging system - Firebase Realtime Database integration - Railway worker backend service UI/UX improvements: - Material Design 3 components - Badge indicators for active pumps - Conditional rendering (show only when active) - Responsive layouts with Wrap widgets - Color-coded status indicators Backend fixes: - Fixed water_flow reading from Firebase - Fixed threshold model copyWith method - Added pompa_pengaduk support across all models - Smart mode with per-pot independent monitoring - Cooldown per-threshold (not per-pot) - Valves stop together when targets reached Renamed: All 'Pupuk' references to 'Larutan Nutrisi Cair'
This commit is contained in:
parent
1224c8e867
commit
6c3b2f3112
|
|
@ -8,6 +8,7 @@ class JadwalModel {
|
||||||
final List<int> potAktif; // Array pot yang aktif [1, 2, 3, 4, 5]
|
final List<int> potAktif; // Array pot yang aktif [1, 2, 3, 4, 5]
|
||||||
final bool pompaAir;
|
final bool pompaAir;
|
||||||
final bool pompaPupuk;
|
final bool pompaPupuk;
|
||||||
|
final bool pompaPengaduk;
|
||||||
|
|
||||||
JadwalModel({
|
JadwalModel({
|
||||||
required this.id,
|
required this.id,
|
||||||
|
|
@ -17,6 +18,7 @@ class JadwalModel {
|
||||||
this.potAktif = const [],
|
this.potAktif = const [],
|
||||||
this.pompaAir = true,
|
this.pompaAir = true,
|
||||||
this.pompaPupuk = false,
|
this.pompaPupuk = false,
|
||||||
|
this.pompaPengaduk = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Create from Firebase JSON
|
/// Create from Firebase JSON
|
||||||
|
|
@ -29,6 +31,7 @@ class JadwalModel {
|
||||||
potAktif: _parsePotAktif(json['pot_aktif']),
|
potAktif: _parsePotAktif(json['pot_aktif']),
|
||||||
pompaAir: json['pompa_air'] ?? true,
|
pompaAir: json['pompa_air'] ?? true,
|
||||||
pompaPupuk: json['pompa_pupuk'] ?? false,
|
pompaPupuk: json['pompa_pupuk'] ?? false,
|
||||||
|
pompaPengaduk: json['pompa_pengaduk'] ?? false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -52,6 +55,7 @@ class JadwalModel {
|
||||||
'pot_aktif': potAktif,
|
'pot_aktif': potAktif,
|
||||||
'pompa_air': pompaAir,
|
'pompa_air': pompaAir,
|
||||||
'pompa_pupuk': pompaPupuk,
|
'pompa_pupuk': pompaPupuk,
|
||||||
|
'pompa_pengaduk': pompaPengaduk,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,6 +68,7 @@ class JadwalModel {
|
||||||
List<int>? potAktif,
|
List<int>? potAktif,
|
||||||
bool? pompaAir,
|
bool? pompaAir,
|
||||||
bool? pompaPupuk,
|
bool? pompaPupuk,
|
||||||
|
bool? pompaPengaduk,
|
||||||
}) {
|
}) {
|
||||||
return JadwalModel(
|
return JadwalModel(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
|
|
@ -73,6 +78,7 @@ class JadwalModel {
|
||||||
potAktif: potAktif ?? this.potAktif,
|
potAktif: potAktif ?? this.potAktif,
|
||||||
pompaAir: pompaAir ?? this.pompaAir,
|
pompaAir: pompaAir ?? this.pompaAir,
|
||||||
pompaPupuk: pompaPupuk ?? this.pompaPupuk,
|
pompaPupuk: pompaPupuk ?? this.pompaPupuk,
|
||||||
|
pompaPengaduk: pompaPengaduk ?? this.pompaPengaduk,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ class ThresholdModel {
|
||||||
final List<int> potAktif; // [1,2,3] atau [4,5]
|
final List<int> potAktif; // [1,2,3] atau [4,5]
|
||||||
final bool pompaAir;
|
final bool pompaAir;
|
||||||
final bool pompaPupuk;
|
final bool pompaPupuk;
|
||||||
|
final bool pompaPengaduk;
|
||||||
|
|
||||||
ThresholdModel({
|
ThresholdModel({
|
||||||
required this.id,
|
required this.id,
|
||||||
|
|
@ -19,6 +20,7 @@ class ThresholdModel {
|
||||||
required this.potAktif,
|
required this.potAktif,
|
||||||
required this.pompaAir,
|
required this.pompaAir,
|
||||||
required this.pompaPupuk,
|
required this.pompaPupuk,
|
||||||
|
required this.pompaPengaduk,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Parse dari Firebase
|
// Parse dari Firebase
|
||||||
|
|
@ -26,7 +28,9 @@ class ThresholdModel {
|
||||||
List<int> parsePotAktif(dynamic potData) {
|
List<int> parsePotAktif(dynamic potData) {
|
||||||
if (potData == null) return [];
|
if (potData == null) return [];
|
||||||
if (potData is List) {
|
if (potData is List) {
|
||||||
return potData.map((e) => e is int ? e : int.tryParse(e.toString()) ?? 0).toList();
|
return potData
|
||||||
|
.map((e) => e is int ? e : int.tryParse(e.toString()) ?? 0)
|
||||||
|
.toList();
|
||||||
}
|
}
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
@ -41,6 +45,7 @@ class ThresholdModel {
|
||||||
potAktif: parsePotAktif(json['pot_aktif']),
|
potAktif: parsePotAktif(json['pot_aktif']),
|
||||||
pompaAir: json['pompa_air'] == true,
|
pompaAir: json['pompa_air'] == true,
|
||||||
pompaPupuk: json['pompa_pupuk'] == true,
|
pompaPupuk: json['pompa_pupuk'] == true,
|
||||||
|
pompaPengaduk: json['pompa_pengaduk'] == true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -55,6 +60,7 @@ class ThresholdModel {
|
||||||
'pot_aktif': potAktif,
|
'pot_aktif': potAktif,
|
||||||
'pompa_air': pompaAir,
|
'pompa_air': pompaAir,
|
||||||
'pompa_pupuk': pompaPupuk,
|
'pompa_pupuk': pompaPupuk,
|
||||||
|
'pompa_pengaduk': pompaPengaduk,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -100,6 +106,7 @@ class ThresholdModel {
|
||||||
List<int>? potAktif,
|
List<int>? potAktif,
|
||||||
bool? pompaAir,
|
bool? pompaAir,
|
||||||
bool? pompaPupuk,
|
bool? pompaPupuk,
|
||||||
|
bool? pompaPengaduk,
|
||||||
}) {
|
}) {
|
||||||
return ThresholdModel(
|
return ThresholdModel(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
|
|
@ -111,6 +118,7 @@ class ThresholdModel {
|
||||||
potAktif: potAktif ?? this.potAktif,
|
potAktif: potAktif ?? this.potAktif,
|
||||||
pompaAir: pompaAir ?? this.pompaAir,
|
pompaAir: pompaAir ?? this.pompaAir,
|
||||||
pompaPupuk: pompaPupuk ?? this.pompaPupuk,
|
pompaPupuk: pompaPupuk ?? this.pompaPupuk,
|
||||||
|
pompaPengaduk: pompaPengaduk ?? this.pompaPengaduk,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -336,6 +336,7 @@ class _DashboardPageState extends State<DashboardPage> {
|
||||||
final suhu = data['suhu'] ?? '0';
|
final suhu = data['suhu'] ?? '0';
|
||||||
final kelembapan = data['kelembapan'] ?? '0';
|
final kelembapan = data['kelembapan'] ?? '0';
|
||||||
final ldr = data['ldr'] ?? '0';
|
final ldr = data['ldr'] ?? '0';
|
||||||
|
final waterFlow = data['water_flow'] ?? 0;
|
||||||
final soil1 = data['soil_1'] ?? '0';
|
final soil1 = data['soil_1'] ?? '0';
|
||||||
final soil2 = data['soil_2'] ?? '0';
|
final soil2 = data['soil_2'] ?? '0';
|
||||||
final soil3 = data['soil_3'] ?? '0';
|
final soil3 = data['soil_3'] ?? '0';
|
||||||
|
|
@ -347,8 +348,14 @@ class _DashboardPageState extends State<DashboardPage> {
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Connection Status Indicator
|
// Connection Status & Water Status Indicators
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
_buildConnectionStatus(),
|
_buildConnectionStatus(),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
_buildWaterFlowStatus(waterFlow),
|
||||||
|
],
|
||||||
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
// Sensor Cards - Temperature, Humidity, Light
|
// Sensor Cards - Temperature, Humidity, Light
|
||||||
|
|
@ -434,6 +441,71 @@ class _DashboardPageState extends State<DashboardPage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildWaterFlowStatus(dynamic waterFlow) {
|
||||||
|
// Debug: Print raw value
|
||||||
|
print(
|
||||||
|
'🔍 DEBUG Water Flow - Raw value: $waterFlow, Type: ${waterFlow.runtimeType}',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Parse waterFlow to int - handles all cases
|
||||||
|
int flowValue = 0;
|
||||||
|
|
||||||
|
if (waterFlow == null) {
|
||||||
|
flowValue = 0;
|
||||||
|
print(' → Water flow is NULL');
|
||||||
|
} else if (waterFlow is int) {
|
||||||
|
flowValue = waterFlow;
|
||||||
|
print(' → Parsed as int: $flowValue');
|
||||||
|
} else if (waterFlow is double) {
|
||||||
|
flowValue = waterFlow.toInt();
|
||||||
|
print(' → Parsed as double to int: $flowValue');
|
||||||
|
} else if (waterFlow is String) {
|
||||||
|
flowValue = int.tryParse(waterFlow) ?? 0;
|
||||||
|
print(' → Parsed from string: $flowValue');
|
||||||
|
} else {
|
||||||
|
final stringValue = waterFlow.toString();
|
||||||
|
flowValue = int.tryParse(stringValue) ?? 0;
|
||||||
|
print(' → Parsed from toString(): $flowValue');
|
||||||
|
}
|
||||||
|
|
||||||
|
final hasWater = flowValue > 0;
|
||||||
|
print(' → Final: flowValue=$flowValue, hasWater=$hasWater');
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color:
|
||||||
|
hasWater
|
||||||
|
? Colors.green.withOpacity(0.1)
|
||||||
|
: Colors.red.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(
|
||||||
|
color: hasWater ? Colors.green : Colors.red,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.water_drop,
|
||||||
|
size: 14,
|
||||||
|
color: hasWater ? Colors.green : Colors.red,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
hasWater ? 'Air Tersedia' : 'Air Habis',
|
||||||
|
style: TextStyle(
|
||||||
|
color: hasWater ? Colors.green : Colors.red,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildSensorCard({
|
Widget _buildSensorCard({
|
||||||
required String title,
|
required String title,
|
||||||
required String value,
|
required String value,
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,7 @@ class _JadwalFormPageState extends State<JadwalFormPage> {
|
||||||
late List<bool> _potSelection; // [pot1, pot2, pot3, pot4, pot5]
|
late List<bool> _potSelection; // [pot1, pot2, pot3, pot4, pot5]
|
||||||
late bool _pompaAir;
|
late bool _pompaAir;
|
||||||
late bool _pompaPupuk;
|
late bool _pompaPupuk;
|
||||||
|
late bool _pompaPengaduk;
|
||||||
late bool _aktif;
|
late bool _aktif;
|
||||||
|
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
|
|
@ -58,6 +59,7 @@ class _JadwalFormPageState extends State<JadwalFormPage> {
|
||||||
|
|
||||||
_pompaAir = jadwal.pompaAir;
|
_pompaAir = jadwal.pompaAir;
|
||||||
_pompaPupuk = jadwal.pompaPupuk;
|
_pompaPupuk = jadwal.pompaPupuk;
|
||||||
|
_pompaPengaduk = jadwal.pompaPengaduk;
|
||||||
_aktif = jadwal.aktif;
|
_aktif = jadwal.aktif;
|
||||||
} else {
|
} else {
|
||||||
// Default values for new jadwal
|
// Default values for new jadwal
|
||||||
|
|
@ -67,6 +69,7 @@ class _JadwalFormPageState extends State<JadwalFormPage> {
|
||||||
_potSelection = [false, false, false, false, false];
|
_potSelection = [false, false, false, false, false];
|
||||||
_pompaAir = true;
|
_pompaAir = true;
|
||||||
_pompaPupuk = false;
|
_pompaPupuk = false;
|
||||||
|
_pompaPengaduk = false;
|
||||||
_aktif = true;
|
_aktif = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -150,6 +153,7 @@ class _JadwalFormPageState extends State<JadwalFormPage> {
|
||||||
potAktif: _selectedPots,
|
potAktif: _selectedPots,
|
||||||
pompaAir: _pompaAir,
|
pompaAir: _pompaAir,
|
||||||
pompaPupuk: _pompaPupuk,
|
pompaPupuk: _pompaPupuk,
|
||||||
|
pompaPengaduk: _pompaPengaduk,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if time slot is taken (for new or time changed)
|
// Check if time slot is taken (for new or time changed)
|
||||||
|
|
@ -603,14 +607,26 @@ class _JadwalFormPageState extends State<JadwalFormPage> {
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
value: _pompaPupuk,
|
value: _pompaPupuk,
|
||||||
onChanged: (value) => setState(() => _pompaPupuk = value),
|
onChanged: (value) => setState(() => _pompaPupuk = value),
|
||||||
title: const Text('Pompa Pupuk'),
|
title: const Text('Pompa Larutan Nutrisi'),
|
||||||
subtitle: const Text('Mengalirkan pupuk/nutrisi'),
|
subtitle: const Text('Mengalirkan larutan nutrisi cair'),
|
||||||
secondary: Icon(
|
secondary: Icon(
|
||||||
Icons.science,
|
Icons.science,
|
||||||
color: _pompaPupuk ? Colors.orange : Colors.grey,
|
color: _pompaPupuk ? Colors.orange : Colors.grey,
|
||||||
),
|
),
|
||||||
activeColor: AppColors.primaryGreen,
|
activeColor: AppColors.primaryGreen,
|
||||||
),
|
),
|
||||||
|
const Divider(),
|
||||||
|
SwitchListTile(
|
||||||
|
value: _pompaPengaduk,
|
||||||
|
onChanged: (value) => setState(() => _pompaPengaduk = value),
|
||||||
|
title: const Text('Pompa Pengaduk'),
|
||||||
|
subtitle: const Text('Mengaduk larutan nutrisi'),
|
||||||
|
secondary: Icon(
|
||||||
|
Icons.blender,
|
||||||
|
color: _pompaPengaduk ? Colors.purple : Colors.grey,
|
||||||
|
),
|
||||||
|
activeColor: AppColors.primaryGreen,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -165,12 +165,6 @@ class _JadwalManagementPageState extends State<JadwalManagementPage> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton.extended(
|
|
||||||
onPressed: () => _navigateToForm(),
|
|
||||||
backgroundColor: AppColors.primaryGreen,
|
|
||||||
icon: const Icon(Icons.add),
|
|
||||||
label: const Text('Tambah Jadwal'),
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -265,8 +259,30 @@ class _JadwalManagementPageState extends State<JadwalManagementPage> {
|
||||||
onRefresh: _loadJadwal,
|
onRefresh: _loadJadwal,
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
itemCount: _jadwalList.length,
|
itemCount: _jadwalList.length + 1, // +1 for add button
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
|
if (index == _jadwalList.length) {
|
||||||
|
// Add button at the bottom
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8, bottom: 80),
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () => _navigateToForm(),
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: const Text('Tambah Jadwal Baru'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColors.primaryGreen,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
final jadwal = _jadwalList[index];
|
final jadwal = _jadwalList[index];
|
||||||
return _buildJadwalCard(jadwal);
|
return _buildJadwalCard(jadwal);
|
||||||
},
|
},
|
||||||
|
|
@ -369,22 +385,95 @@ class _JadwalManagementPageState extends State<JadwalManagementPage> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
// Pumps
|
// Pumps - Display only if enabled
|
||||||
Row(
|
Wrap(
|
||||||
|
spacing: 8,
|
||||||
|
runSpacing: 8,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
if (jadwal.pompaAir)
|
||||||
child: _buildPumpChip(
|
Container(
|
||||||
'Pompa Air',
|
padding: const EdgeInsets.symmetric(
|
||||||
jadwal.pompaAir,
|
horizontal: 8,
|
||||||
Icons.water_drop,
|
vertical: 4,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.blue[50],
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
border: Border.all(color: Colors.blue[300]!),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.water,
|
||||||
|
size: 14,
|
||||||
|
color: Colors.blue[700],
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'Air',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.blue[900],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
],
|
||||||
Expanded(
|
),
|
||||||
child: _buildPumpChip(
|
),
|
||||||
'Pompa Pupuk',
|
if (jadwal.pompaPupuk)
|
||||||
jadwal.pompaPupuk,
|
Container(
|
||||||
Icons.science,
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.green[50],
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
border: Border.all(color: Colors.green[300]!),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.eco, size: 14, color: Colors.green[700]),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'Larutan Nutrisi',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.green[900],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (jadwal.pompaPengaduk)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.purple[50],
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
border: Border.all(color: Colors.purple[300]!),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.blender,
|
||||||
|
size: 14,
|
||||||
|
color: Colors.purple[700],
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'Pengaduk',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.purple[900],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
@ -450,42 +539,4 @@ class _JadwalManagementPageState extends State<JadwalManagementPage> {
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPumpChip(String label, bool enabled, IconData icon) {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color:
|
|
||||||
enabled
|
|
||||||
? AppColors.primaryGreen.withOpacity(0.1)
|
|
||||||
: Colors.grey[200],
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
border: Border.all(
|
|
||||||
color: enabled ? AppColors.primaryGreen : Colors.grey[400]!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
icon,
|
|
||||||
size: 16,
|
|
||||||
color: enabled ? AppColors.primaryGreen : Colors.grey[600],
|
|
||||||
),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
label,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: enabled ? AppColors.primaryGreen : Colors.grey[600],
|
|
||||||
),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,8 @@ class _KontrolPageState extends State<KontrolPage> {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => const ThresholdManagementPage(),
|
builder:
|
||||||
|
(context) => const ThresholdManagementPage(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -156,7 +156,8 @@ class _PotSelectionPageState extends State<PotSelectionPage> {
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
Navigator.of(context).pushReplacement(
|
Navigator.of(context).pushReplacement(
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => const ThresholdManagementPage(),
|
builder:
|
||||||
|
(context) => const ThresholdManagementPage(),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ class _ThresholdFormPageState extends State<ThresholdFormPage> {
|
||||||
late List<bool> _potSelection; // [pot1, pot2, pot3, pot4, pot5]
|
late List<bool> _potSelection; // [pot1, pot2, pot3, pot4, pot5]
|
||||||
late bool _pompaAir;
|
late bool _pompaAir;
|
||||||
late bool _pompaPupuk;
|
late bool _pompaPupuk;
|
||||||
|
late bool _pompaPengaduk;
|
||||||
late bool _aktif;
|
late bool _aktif;
|
||||||
|
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
|
|
@ -54,6 +55,7 @@ class _ThresholdFormPageState extends State<ThresholdFormPage> {
|
||||||
|
|
||||||
_pompaAir = threshold.pompaAir;
|
_pompaAir = threshold.pompaAir;
|
||||||
_pompaPupuk = threshold.pompaPupuk;
|
_pompaPupuk = threshold.pompaPupuk;
|
||||||
|
_pompaPengaduk = threshold.pompaPengaduk;
|
||||||
_aktif = threshold.aktif;
|
_aktif = threshold.aktif;
|
||||||
} else {
|
} else {
|
||||||
// Default values for new threshold
|
// Default values for new threshold
|
||||||
|
|
@ -65,6 +67,7 @@ class _ThresholdFormPageState extends State<ThresholdFormPage> {
|
||||||
_potSelection = [false, false, false, false, false];
|
_potSelection = [false, false, false, false, false];
|
||||||
_pompaAir = true;
|
_pompaAir = true;
|
||||||
_pompaPupuk = false;
|
_pompaPupuk = false;
|
||||||
|
_pompaPengaduk = false;
|
||||||
_aktif = true;
|
_aktif = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -130,6 +133,7 @@ class _ThresholdFormPageState extends State<ThresholdFormPage> {
|
||||||
potAktif: _selectedPots,
|
potAktif: _selectedPots,
|
||||||
pompaAir: _pompaAir,
|
pompaAir: _pompaAir,
|
||||||
pompaPupuk: _pompaPupuk,
|
pompaPupuk: _pompaPupuk,
|
||||||
|
pompaPengaduk: _pompaPengaduk,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Save to Firebase
|
// Save to Firebase
|
||||||
|
|
@ -185,7 +189,8 @@ class _ThresholdFormPageState extends State<ThresholdFormPage> {
|
||||||
title: Text(_isEditMode ? 'Edit Threshold' : 'Tambah Threshold'),
|
title: Text(_isEditMode ? 'Edit Threshold' : 'Tambah Threshold'),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
),
|
),
|
||||||
body: _isLoading
|
body:
|
||||||
|
_isLoading
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: Form(
|
: Form(
|
||||||
key: _formKey,
|
key: _formKey,
|
||||||
|
|
@ -527,8 +532,25 @@ class _ThresholdFormPageState extends State<ThresholdFormPage> {
|
||||||
_pompaPupuk = value;
|
_pompaPupuk = value;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
title: const Text('Pompa Pupuk'),
|
title: const Text('Pompa Larutan Nutrisi'),
|
||||||
subtitle: const Text('Aktifkan pompa pupuk saat penyiraman'),
|
subtitle: const Text(
|
||||||
|
'Aktifkan pompa larutan nutrisi saat penyiraman',
|
||||||
|
),
|
||||||
|
activeColor: AppColor.primary,
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
|
||||||
|
SwitchListTile(
|
||||||
|
value: _pompaPengaduk,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
_pompaPengaduk = value;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
title: const Text('Pompa Pengaduk'),
|
||||||
|
subtitle: const Text(
|
||||||
|
'Aktifkan pompa pengaduk untuk mengaduk larutan',
|
||||||
|
),
|
||||||
activeColor: AppColor.primary,
|
activeColor: AppColor.primary,
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
),
|
),
|
||||||
|
|
@ -567,12 +589,11 @@ class _ThresholdFormPageState extends State<ThresholdFormPage> {
|
||||||
backgroundColor: AppColor.primary,
|
backgroundColor: AppColor.primary,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: Colors.white,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
elevation: 2,
|
elevation: 2,
|
||||||
),
|
),
|
||||||
child: _isLoading
|
child:
|
||||||
|
_isLoading
|
||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
height: 20,
|
height: 20,
|
||||||
width: 20,
|
width: 20,
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ class _ThresholdManagementPageState extends State<ThresholdManagementPage> {
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_loadThreshold();
|
_loadThreshold();
|
||||||
|
_loadSensorModeStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadThreshold() async {
|
Future<void> _loadThreshold() async {
|
||||||
|
|
@ -38,6 +39,23 @@ class _ThresholdManagementPageState extends State<ThresholdManagementPage> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _loadSensorModeStatus() async {
|
||||||
|
final status = await ThresholdService.getSensorModeStatus();
|
||||||
|
setState(() => _sensorModeEnabled = status);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _toggleSensorMode(bool value) async {
|
||||||
|
final success = await ThresholdService.setSensorModeStatus(value);
|
||||||
|
if (success) {
|
||||||
|
setState(() => _sensorModeEnabled = value);
|
||||||
|
_showSuccess(
|
||||||
|
value ? 'Mode Sensor Diaktifkan' : 'Mode Sensor Dinonaktifkan',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_showError('Gagal mengubah mode sensor');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _toggleThresholdAktif(ThresholdModel threshold) async {
|
Future<void> _toggleThresholdAktif(ThresholdModel threshold) async {
|
||||||
final newStatus = !threshold.aktif;
|
final newStatus = !threshold.aktif;
|
||||||
final success = await ThresholdService.toggleThresholdAktif(
|
final success = await ThresholdService.toggleThresholdAktif(
|
||||||
|
|
@ -98,7 +116,8 @@ class _ThresholdManagementPageState extends State<ThresholdManagementPage> {
|
||||||
Future<bool?> _showConfirmDialog(String title, String message) {
|
Future<bool?> _showConfirmDialog(String title, String message) {
|
||||||
return showDialog<bool>(
|
return showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder:
|
||||||
|
(context) => AlertDialog(
|
||||||
title: Text(title),
|
title: Text(title),
|
||||||
content: Text(message),
|
content: Text(message),
|
||||||
actions: [
|
actions: [
|
||||||
|
|
@ -152,19 +171,79 @@ class _ThresholdManagementPageState extends State<ThresholdManagementPage> {
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: _isLoading
|
body: Column(
|
||||||
|
children: [
|
||||||
|
_buildSensorModeCard(),
|
||||||
|
Expanded(
|
||||||
|
child:
|
||||||
|
_isLoading
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: RefreshIndicator(
|
: _thresholdList.isEmpty
|
||||||
onRefresh: _loadThreshold,
|
|
||||||
child: _thresholdList.isEmpty
|
|
||||||
? _buildEmptyState()
|
? _buildEmptyState()
|
||||||
: _buildThresholdList(),
|
: _buildThresholdList(),
|
||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton.extended(
|
],
|
||||||
onPressed: () => _navigateToForm(),
|
),
|
||||||
backgroundColor: AppColor.primary,
|
);
|
||||||
icon: const Icon(Icons.add),
|
}
|
||||||
label: const Text('Tambah Threshold'),
|
|
||||||
|
Widget _buildSensorModeCard() {
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.all(16),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.05),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color:
|
||||||
|
_sensorModeEnabled
|
||||||
|
? AppColor.primary.withOpacity(0.1)
|
||||||
|
: Colors.grey.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.sensors,
|
||||||
|
color: _sensorModeEnabled ? AppColor.primary : Colors.grey,
|
||||||
|
size: 28,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Mode Sensor',
|
||||||
|
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
_sensorModeEnabled
|
||||||
|
? 'Monitoring otomatis aktif'
|
||||||
|
: 'Monitoring otomatis nonaktif',
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Switch(
|
||||||
|
value: _sensorModeEnabled,
|
||||||
|
onChanged: _toggleSensorMode,
|
||||||
|
activeColor: AppColor.primary,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -195,13 +274,38 @@ class _ThresholdManagementPageState extends State<ThresholdManagementPage> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildThresholdList() {
|
Widget _buildThresholdList() {
|
||||||
return ListView.builder(
|
return RefreshIndicator(
|
||||||
|
onRefresh: _loadThreshold,
|
||||||
|
child: ListView.builder(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
itemCount: _thresholdList.length,
|
itemCount: _thresholdList.length + 1, // +1 for add button
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
|
if (index == _thresholdList.length) {
|
||||||
|
// Add button at the bottom
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 8, bottom: 80),
|
||||||
|
child: SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: ElevatedButton.icon(
|
||||||
|
onPressed: () => _navigateToForm(),
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: const Text('Tambah Threshold Baru'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColor.primary,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
final threshold = _thresholdList[index];
|
final threshold = _thresholdList[index];
|
||||||
return _buildThresholdCard(threshold);
|
return _buildThresholdCard(threshold);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -233,7 +337,10 @@ class _ThresholdManagementPageState extends State<ThresholdManagementPage> {
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
Icons.sensors,
|
Icons.sensors,
|
||||||
color: threshold.aktif ? AppColors.primaryGreen : Colors.grey,
|
color:
|
||||||
|
threshold.aktif
|
||||||
|
? AppColors.primaryGreen
|
||||||
|
: Colors.grey,
|
||||||
size: 24,
|
size: 24,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -383,7 +490,7 @@ class _ThresholdManagementPageState extends State<ThresholdManagementPage> {
|
||||||
Icon(Icons.eco, size: 14, color: Colors.green[700]),
|
Icon(Icons.eco, size: 14, color: Colors.green[700]),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
'Pupuk',
|
'Larutan Nutrisi',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: Colors.green[900],
|
color: Colors.green[900],
|
||||||
|
|
@ -392,6 +499,39 @@ class _ThresholdManagementPageState extends State<ThresholdManagementPage> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if ((threshold.pompaAir || threshold.pompaPupuk) &&
|
||||||
|
threshold.pompaPengaduk)
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
if (threshold.pompaPengaduk)
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.purple[50],
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
border: Border.all(color: Colors.purple[300]!),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.blender,
|
||||||
|
size: 14,
|
||||||
|
color: Colors.purple[700],
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'Pengaduk',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.purple[900],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|
@ -399,33 +539,33 @@ class _ThresholdManagementPageState extends State<ThresholdManagementPage> {
|
||||||
|
|
||||||
// Action buttons
|
// Action buttons
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
children: [
|
||||||
TextButton.icon(
|
Expanded(
|
||||||
|
child: OutlinedButton.icon(
|
||||||
onPressed: () => _navigateToForm(threshold: threshold),
|
onPressed: () => _navigateToForm(threshold: threshold),
|
||||||
icon: const Icon(Icons.edit, size: 18),
|
icon: const Icon(Icons.edit, size: 18),
|
||||||
label: const Text('Edit'),
|
label: const Text('Edit'),
|
||||||
style: TextButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
foregroundColor: Colors.blue,
|
foregroundColor: AppColor.primary,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
TextButton.icon(
|
Expanded(
|
||||||
|
child: OutlinedButton.icon(
|
||||||
onPressed: () => _duplicateThreshold(threshold),
|
onPressed: () => _duplicateThreshold(threshold),
|
||||||
icon: const Icon(Icons.copy, size: 18),
|
icon: const Icon(Icons.copy, size: 18),
|
||||||
label: const Text('Duplikat'),
|
label: const Text('Duplikat'),
|
||||||
style: TextButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
foregroundColor: Colors.orange,
|
foregroundColor: Colors.blue,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
TextButton.icon(
|
IconButton(
|
||||||
onPressed: () => _deleteThreshold(threshold),
|
onPressed: () => _deleteThreshold(threshold),
|
||||||
icon: const Icon(Icons.delete, size: 18),
|
icon: const Icon(Icons.delete),
|
||||||
label: const Text('Hapus'),
|
color: Colors.red,
|
||||||
style: TextButton.styleFrom(
|
|
||||||
foregroundColor: Colors.red,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ class FirebaseDatabaseService {
|
||||||
'suhu': _parseValue(data['suhu']),
|
'suhu': _parseValue(data['suhu']),
|
||||||
'kelembapan': _parseValue(data['kelembapan']),
|
'kelembapan': _parseValue(data['kelembapan']),
|
||||||
'ldr': _parseValue(data['ldr']),
|
'ldr': _parseValue(data['ldr']),
|
||||||
|
'water_flow': data['water_flow'] ?? 0, // TAMBAHAN: water flow sensor
|
||||||
'soil_1': _parseValue(data['soil_1']),
|
'soil_1': _parseValue(data['soil_1']),
|
||||||
'soil_2': _parseValue(data['soil_2']),
|
'soil_2': _parseValue(data['soil_2']),
|
||||||
'soil_3': _parseValue(data['soil_3']),
|
'soil_3': _parseValue(data['soil_3']),
|
||||||
|
|
@ -40,6 +41,7 @@ class FirebaseDatabaseService {
|
||||||
'suhu': _parseValue(data['suhu']),
|
'suhu': _parseValue(data['suhu']),
|
||||||
'kelembapan': _parseValue(data['kelembapan']),
|
'kelembapan': _parseValue(data['kelembapan']),
|
||||||
'ldr': _parseValue(data['ldr']),
|
'ldr': _parseValue(data['ldr']),
|
||||||
|
'water_flow': data['water_flow'] ?? 0, // TAMBAHAN: water flow sensor
|
||||||
'soil_1': _parseValue(data['soil_1']),
|
'soil_1': _parseValue(data['soil_1']),
|
||||||
'soil_2': _parseValue(data['soil_2']),
|
'soil_2': _parseValue(data['soil_2']),
|
||||||
'soil_3': _parseValue(data['soil_3']),
|
'soil_3': _parseValue(data['soil_3']),
|
||||||
|
|
|
||||||
|
|
@ -175,11 +175,14 @@ class ThresholdService {
|
||||||
if (threshold.id == currentId) continue;
|
if (threshold.id == currentId) continue;
|
||||||
|
|
||||||
// Cek apakah ada pot yang sama
|
// Cek apakah ada pot yang sama
|
||||||
final hasSamePot = threshold.potAktif.any((pot) => potAktif.contains(pot));
|
final hasSamePot = threshold.potAktif.any(
|
||||||
|
(pot) => potAktif.contains(pot),
|
||||||
|
);
|
||||||
if (!hasSamePot) continue;
|
if (!hasSamePot) continue;
|
||||||
|
|
||||||
// Cek apakah range overlap
|
// Cek apakah range overlap
|
||||||
final rangeOverlap = !(batasAtas < threshold.batasBawah ||
|
final rangeOverlap =
|
||||||
|
!(batasAtas < threshold.batasBawah ||
|
||||||
batasBawah > threshold.batasAtas);
|
batasBawah > threshold.batasAtas);
|
||||||
|
|
||||||
if (rangeOverlap) return true;
|
if (rangeOverlap) return true;
|
||||||
|
|
@ -212,4 +215,27 @@ class ThresholdService {
|
||||||
return 'threshold_1';
|
return 'threshold_1';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get sensor mode status (otomatis)
|
||||||
|
static Future<bool> getSensorModeStatus() async {
|
||||||
|
try {
|
||||||
|
final snapshot = await _dbRef.child(kontrolPath).child('otomatis').get();
|
||||||
|
return snapshot.value as bool? ?? false;
|
||||||
|
} catch (e) {
|
||||||
|
print('Error getting sensor mode status: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set sensor mode status (otomatis)
|
||||||
|
static Future<bool> setSensorModeStatus(bool enabled) async {
|
||||||
|
try {
|
||||||
|
await _dbRef.child(kontrolPath).child('otomatis').set(enabled);
|
||||||
|
print('✅ Sensor mode set to ${enabled ? "enabled" : "disabled"}');
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ Error setting sensor mode: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -133,20 +133,24 @@ const wateringQueue = new Queue('watering', { connection: redis });
|
||||||
redis.on('connect', () => console.log('✅ Redis connected'));
|
redis.on('connect', () => console.log('✅ Redis connected'));
|
||||||
redis.on('error', (err) => console.error('❌ Redis error:', err.message));
|
redis.on('error', (err) => console.error('❌ Redis error:', err.message));
|
||||||
|
|
||||||
// Track last watering time untuk prevent spam
|
// Track last watering time PER-THRESHOLD (not per-pot!) untuk prevent spam
|
||||||
const lastWateringTime = {};
|
const lastThresholdTime = {};
|
||||||
|
|
||||||
// ==================== WATERING WORKER ====================
|
// ==================== WATERING WORKER ====================
|
||||||
|
|
||||||
const wateringWorker = new Worker(
|
const wateringWorker = new Worker(
|
||||||
'watering',
|
'watering',
|
||||||
async (job) => {
|
async (job) => {
|
||||||
const { type, potNumbers, pompaAir, pompaPupuk, duration, scheduleId } = job.data;
|
const { type, potNumbers, pompaAir, pompaPupuk, duration, scheduleId, thresholdId, smartMode, sensorData } = job.data;
|
||||||
|
|
||||||
console.log(`\n💧 Processing Job: ${job.id}`);
|
console.log(`\n💧 Processing Job: ${job.id}`);
|
||||||
console.log(` Type: ${type}`);
|
console.log(` Type: ${type}`);
|
||||||
console.log(` Pots: [${potNumbers.join(', ')}]`);
|
console.log(` Pots: [${potNumbers.join(', ')}]`);
|
||||||
console.log(` Duration: ${duration}s`);
|
console.log(` Mode: ${smartMode ? 'SMART (auto-stop at target)' : 'FIXED'}`);
|
||||||
|
console.log(` Duration: ${duration}s ${smartMode ? '(max)' : ''}`);
|
||||||
|
if (sensorData) {
|
||||||
|
console.log(` Target: ${sensorData.batasBawah}% → ${sensorData.batasAtas}%`);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Prepare aktuator updates
|
// Prepare aktuator updates
|
||||||
|
|
@ -167,10 +171,91 @@ const wateringWorker = new Worker(
|
||||||
console.log(' 📝 Updates:', JSON.stringify(updates, null, 2));
|
console.log(' 📝 Updates:', JSON.stringify(updates, null, 2));
|
||||||
|
|
||||||
await updateFirebaseSmart('aktuator', updates);
|
await updateFirebaseSmart('aktuator', updates);
|
||||||
|
console.log(` 🚀 ALL VALVES STARTED SIMULTANEOUSLY: ${Object.keys(updates).filter(k => k.startsWith('mosvet_')).join(', ')}`);
|
||||||
|
|
||||||
// (Verification skipped to avoid SDK timeout - update success already logged above)
|
// SMART MODE: Monitor sensor and stop pots TOGETHER when they reach target
|
||||||
|
if (smartMode && sensorData && sensorData.batasAtas) {
|
||||||
|
const targetSoil = sensorData.batasAtas;
|
||||||
|
const maxDuration = duration * 1000; // Convert to ms
|
||||||
|
const startTime = Date.now();
|
||||||
|
|
||||||
// Wait for duration with progress logging
|
// Track which pots are still actively watering
|
||||||
|
let activePots = [...potNumbers];
|
||||||
|
|
||||||
|
console.log(` 🎯 SMART MODE: Monitoring ${activePots.length} pots, target ${targetSoil}%...`);
|
||||||
|
console.log(` ⚡ Valves will stop TOGETHER when pots reach target (checked every 2s)`);
|
||||||
|
|
||||||
|
while (activePots.length > 0 && Date.now() - startTime < maxDuration) {
|
||||||
|
await sleep(2000); // Check every 2 seconds
|
||||||
|
|
||||||
|
try {
|
||||||
|
const currentSensorData = await readFirebaseSmart('data');
|
||||||
|
|
||||||
|
if (currentSensorData) {
|
||||||
|
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
||||||
|
const potsToStop = [];
|
||||||
|
|
||||||
|
// Check ALL active pots and collect which ones reached target
|
||||||
|
for (const pot of activePots) {
|
||||||
|
const soilKey = `soil_${pot}`;
|
||||||
|
const currentValue = parseInt(currentSensorData[soilKey]) || 0;
|
||||||
|
|
||||||
|
if (currentValue >= targetSoil) {
|
||||||
|
console.log(` ✅ [${elapsed}s] POT ${pot}: ${currentValue}% >= ${targetSoil}% - TARGET REACHED!`);
|
||||||
|
potsToStop.push(pot);
|
||||||
|
} else {
|
||||||
|
console.log(` ⏳ [${elapsed}s] POT ${pot}: ${currentValue}% < ${targetSoil}% - continuing...`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop ALL pots that reached target TOGETHER (not one-by-one!)
|
||||||
|
if (potsToStop.length > 0) {
|
||||||
|
const stopUpdates = {};
|
||||||
|
for (const pot of potsToStop) {
|
||||||
|
stopUpdates[`mosvet_${pot + 2}`] = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateFirebaseSmart('aktuator', stopUpdates);
|
||||||
|
console.log(` 🔴 STOPPED TOGETHER: ${Object.keys(stopUpdates).join(', ')} (Pots: [${potsToStop.join(', ')}])`);
|
||||||
|
|
||||||
|
// Remove stopped pots from active list
|
||||||
|
activePots = activePots.filter(p => !potsToStop.includes(p));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activePots.length === 0) {
|
||||||
|
console.log(` 🎉 All pots reached target! Smart watering complete.`);
|
||||||
|
} else {
|
||||||
|
console.log(` 📍 Still watering: [${activePots.join(', ')}]`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (sensorError) {
|
||||||
|
console.warn(` ⚠️ Failed to read sensor: ${sensorError.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If any pots still active after max duration (timeout), stop them now TOGETHER
|
||||||
|
if (activePots.length > 0) {
|
||||||
|
console.log(` ⏱️ Max duration ${duration}s reached. Force stopping remaining pots: [${activePots.join(', ')}]`);
|
||||||
|
const timeoutStops = {};
|
||||||
|
for (const pot of activePots) {
|
||||||
|
timeoutStops[`mosvet_${pot + 2}`] = false;
|
||||||
|
}
|
||||||
|
await updateFirebaseSmart('aktuator', timeoutStops);
|
||||||
|
console.log(` 🔴 Force stopped TOGETHER: ${Object.keys(timeoutStops).join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, stop pumps
|
||||||
|
const pumpStop = {};
|
||||||
|
if (pompaAir) pumpStop['mosvet_1'] = false;
|
||||||
|
if (pompaPupuk) pumpStop['mosvet_2'] = false;
|
||||||
|
if (Object.keys(pumpStop).length > 0) {
|
||||||
|
await updateFirebaseSmart('aktuator', pumpStop);
|
||||||
|
console.log(' 🔴 Pumps stopped:', Object.keys(pumpStop).join(', '));
|
||||||
|
}
|
||||||
|
console.log(' ✅ Smart mode completed, now logging history...');
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// FIXED MODE: Wait for fixed duration
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
const endTime = startTime + duration * 1000;
|
const endTime = startTime + duration * 1000;
|
||||||
|
|
||||||
|
|
@ -182,7 +267,7 @@ const wateringWorker = new Worker(
|
||||||
await sleep(1000);
|
await sleep(1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Turn OFF
|
// Turn OFF all at once (FIXED mode only)
|
||||||
const offUpdates = {};
|
const offUpdates = {};
|
||||||
for (const key in updates) {
|
for (const key in updates) {
|
||||||
offUpdates[key] = false;
|
offUpdates[key] = false;
|
||||||
|
|
@ -190,14 +275,16 @@ const wateringWorker = new Worker(
|
||||||
console.log(' 🔴 Turning OFF:', Object.keys(offUpdates).join(', '));
|
console.log(' 🔴 Turning OFF:', Object.keys(offUpdates).join(', '));
|
||||||
await updateFirebaseSmart('aktuator', offUpdates);
|
await updateFirebaseSmart('aktuator', offUpdates);
|
||||||
console.log(' ✅ Turn OFF completed, now logging history...');
|
console.log(' ✅ Turn OFF completed, now logging history...');
|
||||||
|
}
|
||||||
|
|
||||||
// Log history
|
// Log history
|
||||||
await logHistory(type, potNumbers, duration);
|
await logHistory(type, potNumbers, duration);
|
||||||
console.log(' ✅ History logged successfully');
|
console.log(' ✅ History logged successfully');
|
||||||
|
|
||||||
// Update last watering time
|
// Update last watering time PER-THRESHOLD (not per-pot!)
|
||||||
for (const pot of potNumbers) {
|
if (thresholdId) {
|
||||||
lastWateringTime[`pot_${pot}`] = Date.now();
|
lastThresholdTime[thresholdId] = Date.now();
|
||||||
|
console.log(` ⏰ Cooldown set for ${thresholdId} (2 minutes)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(` ✅ Job completed successfully`);
|
console.log(` ✅ Job completed successfully`);
|
||||||
|
|
@ -786,36 +873,44 @@ setTimeout(async () => {
|
||||||
|
|
||||||
let sensorCheckCounter = 0;
|
let sensorCheckCounter = 0;
|
||||||
|
|
||||||
async function setupSensorMonitoring() {
|
// Core sensor check logic (shared by listener and polling)
|
||||||
console.log('✅ Sensor Mode (Threshold System) monitoring started');
|
async function checkSensorThresholds() {
|
||||||
|
|
||||||
db.ref('data').on('value', async (snapshot) => {
|
|
||||||
sensorCheckCounter++;
|
sensorCheckCounter++;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sensorData = snapshot.val();
|
// Fetch sensor data using smart method (REST fallback)
|
||||||
|
const sensorData = await readFirebaseSmart('data');
|
||||||
|
|
||||||
if (!sensorData) {
|
if (!sensorData) {
|
||||||
console.log('⚠️ Sensor data is null/empty');
|
console.log('⚠️ Sensor data is null/empty - ESP32 might not be sending data');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const configSnapshot = await fetchWithTimeout(db.ref(FIREBASE_PATHS.kontrol), 10000);
|
// Fetch kontrol config using smart method
|
||||||
const kontrolConfig = configSnapshot.val();
|
const kontrolConfig = await fetchKontrolSmart();
|
||||||
|
|
||||||
if (!kontrolConfig) {
|
if (!kontrolConfig) {
|
||||||
|
console.log('⚠️ Kontrol config is null');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if sensor mode is enabled (using 'otomatis' field, not deprecated 'sensor')
|
||||||
|
if (!kontrolConfig.otomatis) {
|
||||||
|
if (sensorCheckCounter % 10 === 0) {
|
||||||
|
console.log(`⚠️ Sensor mode DISABLED (otomatis=false). Skipping threshold check.`);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect all threshold_* nodes
|
// Detect all threshold_* nodes
|
||||||
const allThresholds = Object.keys(kontrolConfig).filter(key => key.startsWith('threshold_'));
|
const allThresholds = Object.keys(kontrolConfig).filter(key => key.startsWith('threshold_'));
|
||||||
|
|
||||||
// Log sensor check (verbose hanya setiap 10 kali)
|
// Log sensor check (ALWAYS LOG untuk debugging)
|
||||||
if (sensorCheckCounter % 10 === 0) {
|
|
||||||
console.log(`\n🌡️ SENSOR CHECK #${sensorCheckCounter} | Total Thresholds: ${allThresholds.length}`);
|
console.log(`\n🌡️ SENSOR CHECK #${sensorCheckCounter} | Total Thresholds: ${allThresholds.length}`);
|
||||||
}
|
console.log(` 📊 Sensor Data:`, JSON.stringify(sensorData, null, 2));
|
||||||
|
|
||||||
if (allThresholds.length === 0) {
|
if (allThresholds.length === 0) {
|
||||||
// No thresholds configured
|
console.log(' ⚠️ No thresholds configured');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -823,8 +918,22 @@ async function setupSensorMonitoring() {
|
||||||
for (const thresholdKey of allThresholds) {
|
for (const thresholdKey of allThresholds) {
|
||||||
const threshold = kontrolConfig[thresholdKey];
|
const threshold = kontrolConfig[thresholdKey];
|
||||||
|
|
||||||
|
console.log(`\n 🔍 Checking ${thresholdKey}:`);
|
||||||
|
console.log(` Config:`, JSON.stringify(threshold, null, 2));
|
||||||
|
|
||||||
// Skip if threshold is not active or invalid
|
// Skip if threshold is not active or invalid
|
||||||
if (!threshold || !threshold.aktif) continue;
|
if (!threshold || !threshold.aktif) {
|
||||||
|
console.log(` ❌ Skipped: ${!threshold ? 'Not found' : 'Not active (aktif=false)'}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check THRESHOLD cooldown (not per-pot!)
|
||||||
|
const lastTime = lastThresholdTime[thresholdKey];
|
||||||
|
if (lastTime && Date.now() - lastTime < config.worker.sensorDebounce) {
|
||||||
|
const remainingSeconds = Math.ceil((config.worker.sensorDebounce - (Date.now() - lastTime)) / 1000);
|
||||||
|
console.log(` ⏳ ${thresholdKey}: Cooldown active (${remainingSeconds}s remaining) - skipping entire threshold`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const batasBawah = threshold.batas_bawah || 30;
|
const batasBawah = threshold.batas_bawah || 30;
|
||||||
const batasAtas = threshold.batas_atas || 70;
|
const batasAtas = threshold.batas_atas || 70;
|
||||||
|
|
@ -834,48 +943,66 @@ async function setupSensorMonitoring() {
|
||||||
const pompaAir = threshold.pompa_air === true;
|
const pompaAir = threshold.pompa_air === true;
|
||||||
const pompaPupuk = threshold.pompa_pupuk === true;
|
const pompaPupuk = threshold.pompa_pupuk === true;
|
||||||
|
|
||||||
|
// Collect pots that need watering in this threshold
|
||||||
|
const potsNeedWatering = [];
|
||||||
|
const potDetails = [];
|
||||||
|
|
||||||
// Check each pot in this threshold
|
// Check each pot in this threshold
|
||||||
for (const potNumber of potAktif) {
|
for (const potNumber of potAktif) {
|
||||||
if (potNumber < 1 || potNumber > 5) continue;
|
if (potNumber < 1 || potNumber > 5) {
|
||||||
|
console.log(` ⚠️ POT ${potNumber}: Invalid pot number (must be 1-5)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const soilKey = `soil_${potNumber}`;
|
const soilKey = `soil_${potNumber}`;
|
||||||
const soilValue = parseInt(sensorData[soilKey]) || 0;
|
const soilValue = parseInt(sensorData[soilKey]) || 0;
|
||||||
|
|
||||||
// Check if below threshold
|
console.log(` 🌱 POT ${potNumber} (${soilKey}): ${soilValue}% | Threshold: ${batasBawah}-${batasAtas}%`);
|
||||||
if (soilValue < batasBawah) {
|
console.log(` → Raw value: ${sensorData[soilKey]} | Parsed: ${soilValue} | Check: ${soilValue} < ${batasBawah} = ${soilValue < batasBawah}`);
|
||||||
const potKey = `pot_${potNumber}`;
|
|
||||||
const lastTime = lastWateringTime[potKey];
|
|
||||||
|
|
||||||
// Debounce: minimum 2 menit antar penyiraman
|
// NEW: Check if ABOVE upper threshold - skip if too wet!
|
||||||
if (lastTime && Date.now() - lastTime < config.worker.sensorDebounce) {
|
if (soilValue >= batasAtas) {
|
||||||
const remainingSeconds = Math.ceil((config.worker.sensorDebounce - (Date.now() - lastTime)) / 1000);
|
console.log(` ✅ POT ${potNumber}: SKIP (${soilValue}% >= ${batasAtas}% - sudah basah!)`);
|
||||||
if (sensorCheckCounter % 10 === 0) {
|
|
||||||
console.log(`⏳ ${thresholdKey.toUpperCase()} - POT ${potNumber}: Cooldown (${remainingSeconds}s)`);
|
|
||||||
}
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if below lower threshold
|
||||||
|
if (soilValue < batasBawah) {
|
||||||
|
console.log(` 🚨 POT ${potNumber} KERING! ${soilValue}% < ${batasBawah}%`);
|
||||||
|
|
||||||
|
// Add to watering list (cooldown already checked at threshold level)
|
||||||
|
potsNeedWatering.push(potNumber);
|
||||||
|
potDetails.push({ pot: potNumber, value: soilValue });
|
||||||
|
} else {
|
||||||
|
console.log(` ✅ POT ${potNumber}: OK (${soilValue}% >= ${batasBawah}%)`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NEW: Create SINGLE job for ALL pots that need watering in this threshold
|
||||||
|
if (potsNeedWatering.length > 0) {
|
||||||
console.log(`\n🌡️ THRESHOLD TRIGGERED: ${thresholdKey.toUpperCase()}`);
|
console.log(`\n🌡️ THRESHOLD TRIGGERED: ${thresholdKey.toUpperCase()}`);
|
||||||
console.log(` POT ${potNumber}: ${soilValue}% < ${batasBawah}%`);
|
console.log(` Pots needing water: [${potsNeedWatering.join(', ')}]`);
|
||||||
console.log(` Mode: ${smartMode ? 'Smart' : 'Fixed'}, Target: ${smartMode ? batasAtas + '%' : durasi + 's'}`);
|
potDetails.forEach(p => console.log(` - POT ${p.pot}: ${p.value}% < ${batasBawah}%`));
|
||||||
|
console.log(` Mode: ${smartMode ? 'Smart (monitor until ' + batasAtas + '%)' : 'Fixed (' + durasi + 's)'}`);
|
||||||
console.log(` Pumps: Air=${pompaAir}, Pupuk=${pompaPupuk}`);
|
console.log(` Pumps: Air=${pompaAir}, Pupuk=${pompaPupuk}`);
|
||||||
|
|
||||||
const jobId = `${thresholdKey}-pot-${potNumber}-${Date.now()}`;
|
const jobId = `${thresholdKey}-${Date.now()}`;
|
||||||
await wateringQueue.add(
|
await wateringQueue.add(
|
||||||
`threshold-pot-${potNumber}`,
|
thresholdKey,
|
||||||
{
|
{
|
||||||
type: 'sensor_threshold',
|
type: 'sensor_threshold',
|
||||||
potNumbers: [potNumber],
|
potNumbers: potsNeedWatering, // ALL pots in 1 job!
|
||||||
pompaAir: pompaAir,
|
pompaAir: pompaAir,
|
||||||
pompaPupuk: pompaPupuk,
|
pompaPupuk: pompaPupuk,
|
||||||
duration: durasi,
|
duration: durasi,
|
||||||
scheduleId: jobId,
|
scheduleId: jobId,
|
||||||
thresholdId: thresholdKey,
|
thresholdId: thresholdKey,
|
||||||
|
smartMode: smartMode,
|
||||||
sensorData: {
|
sensorData: {
|
||||||
soilValue,
|
|
||||||
batasBawah,
|
batasBawah,
|
||||||
batasAtas,
|
batasAtas,
|
||||||
mode: smartMode ? 'smart' : 'fixed'
|
mode: smartMode ? 'smart' : 'fixed',
|
||||||
|
potValues: potDetails
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -886,16 +1013,59 @@ async function setupSensorMonitoring() {
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log(` 📌 Added to queue: ${jobId}`);
|
console.log(` 📌 Added to queue: ${jobId}`);
|
||||||
|
console.log(` 🔄 ${thresholdKey} will execute simultaneously for ALL pots`);
|
||||||
// Update last watering time to prevent rapid re-triggering
|
console.log(` ⏰ After completion, ${thresholdKey} cooldown = 2 minutes (other thresholds can still run)`);
|
||||||
lastWateringTime[potKey] = Date.now();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Error in threshold monitoring:', error.message);
|
console.error('❌ Error in sensor threshold check:', error.message);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup sensor monitoring with BOTH listener (SDK) and polling (fallback)
|
||||||
|
async function setupSensorMonitoring() {
|
||||||
|
console.log('🌡️ ==================== SENSOR MODE ENABLED ====================');
|
||||||
|
console.log('✅ Sensor Mode (Threshold System) monitoring started');
|
||||||
|
console.log('📍 Primary: Polling every 30 seconds (REST API)');
|
||||||
|
console.log('📍 Backup: Firebase listener on /data (if SDK works)');
|
||||||
|
console.log('📍 Config path: /' + FIREBASE_PATHS.kontrol);
|
||||||
|
console.log('⏱️ Debounce time: 2 minutes between watering per pot');
|
||||||
|
console.log('================================================================\n');
|
||||||
|
|
||||||
|
// METHOD 1: Polling (RELIABLE - uses REST API)
|
||||||
|
// Check sensor threshold every 30 seconds
|
||||||
|
setInterval(async () => {
|
||||||
|
try {
|
||||||
|
await checkSensorThresholds();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Polling sensor check failed:', error.message);
|
||||||
|
}
|
||||||
|
}, 30000); // 30 seconds
|
||||||
|
|
||||||
|
// Run first check immediately
|
||||||
|
setTimeout(async () => {
|
||||||
|
console.log('🚀 Running first sensor check...');
|
||||||
|
try {
|
||||||
|
await checkSensorThresholds();
|
||||||
|
console.log('✅ First sensor check completed');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ First sensor check failed:', error.message);
|
||||||
|
}
|
||||||
|
}, 10000); // 10 seconds after startup
|
||||||
|
|
||||||
|
// METHOD 2: Firebase Listener (BACKUP - might not work if SDK fails)
|
||||||
|
try {
|
||||||
|
db.ref('data').on('value', async (snapshot) => {
|
||||||
|
console.log('🔔 Firebase listener triggered (SDK working!)');
|
||||||
|
// Call the same check function
|
||||||
|
await checkSensorThresholds();
|
||||||
|
}, (error) => {
|
||||||
|
console.error('❌ Firebase listener error:', error.message);
|
||||||
});
|
});
|
||||||
|
console.log('✅ Firebase listener attached (backup method)');
|
||||||
|
} catch (error) {
|
||||||
|
console.log('⚠️ Firebase listener failed to attach (will rely on polling)');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setupSensorMonitoring();
|
setupSensorMonitoring();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue