tambahan perhitungan
This commit is contained in:
parent
fcef9d7e5d
commit
4f9182e4b4
|
|
@ -22,6 +22,8 @@ class EvaporasiSettingsBloc extends Bloc<EvaporasiSettingsEvent, EvaporasiSettin
|
|||
on<EvaporasiKoreksiOffsetChanged>(_onOffsetChanged);
|
||||
on<EvaporasiPumpStartChanged>(_onPumpStartChanged);
|
||||
on<EvaporasiPumpEndChanged>(_onPumpEndChanged);
|
||||
on<EvaporasiStandarTinggiChanged>(_onStandarTinggiChanged);
|
||||
on<EvaporasiBatasKritisChanged>(_onBatasKritisChanged);
|
||||
on<EvaporasiD0Changed>(_onD0Changed);
|
||||
on<EvaporasiDmaxManualChanged>(_onDmaxManualChanged);
|
||||
on<EvaporasiIntervalRealtimeChanged>(_onIntervalRealtimeChanged);
|
||||
|
|
@ -44,10 +46,16 @@ class EvaporasiSettingsBloc extends Bloc<EvaporasiSettingsEvent, EvaporasiSettin
|
|||
thresholdRendah: _toDouble(data['threshold_rendah'], 2.0),
|
||||
thresholdTinggi: _toDouble(data['threshold_tinggi'], 10.0),
|
||||
rumusKalibrasi: (data['rumus_kalibrasi'] as String?) ?? 'selisih_max',
|
||||
koreksiOffset: _toDouble(data['koreksi_offset'], 0.0), pumpStartTime: (data['pump_start_time'] as String?) ?? '06:00',
|
||||
pumpEndTime: (data['pump_end_time'] as String?) ?? '18:00',
|
||||
koreksiOffset: _toDouble(data['koreksi_offset'], 0.0),
|
||||
pumpStartTime: (data['pump_start_time'] as String?) ??
|
||||
(data['jam_pompa_mulai'] != null ? '${_toInt(data['jam_pompa_mulai'], 6).toString().padLeft(2, '0')}:00' : '06:00'),
|
||||
pumpEndTime: (data['pump_end_time'] as String?) ??
|
||||
(data['jam_pompa_selesai'] != null ? '${_toInt(data['jam_pompa_selesai'], 18).toString().padLeft(2, '0')}:00' : '18:00'),
|
||||
d0: _toInt(data['d0'], 0),
|
||||
dmaxManual: _toInt(data['dmax_manual'], 0), intervalRealtime_ms: _toInt(data['interval_realtime_ms'], 300000),
|
||||
dmaxManual: _toInt(data['dmax_manual'], 0),
|
||||
standarTinggiCm: _toDouble(data['standar_tinggi_cm'] ?? data['standar_tinggi'], 18.0),
|
||||
batasKritisCm: _toDouble(data['batas_kritis_cm'] ?? data['batas_kritis'], 15.0),
|
||||
intervalRealtime_ms: _toInt(data['interval_realtime_ms'], 300000),
|
||||
intervalHistory_ms: _toInt(data['interval_history_ms'], 600000),
|
||||
intervalBaca_ms: _toInt(data['interval_baca_ms'], 10000),
|
||||
status: EvaporasiSettingsStatus.loaded,
|
||||
|
|
@ -91,6 +99,19 @@ class EvaporasiSettingsBloc extends Bloc<EvaporasiSettingsEvent, EvaporasiSettin
|
|||
activeD0: activeD0,
|
||||
activeDmax: activeDmax,
|
||||
lastUpdate: lastUpd,
|
||||
sensorError: _toBool(rt['sensor_error'], state.sensorError),
|
||||
ntpSync: _toBool(rt['ntp_sync'], state.ntpSync),
|
||||
snapshotCm: _toDouble(rt['snapshot_cm'] ?? rt['snapshot'], state.snapshotCm),
|
||||
standarTinggiCm: _toDouble(rt['standar_tinggi'] ?? rt['standar_tinggi_cm'], state.standarTinggiCm),
|
||||
batasKritisCm: _toDouble(rt['batas_kritis'] ?? rt['batas_kritis_cm'], state.batasKritisCm),
|
||||
tempCompActive: _toBool(rt['temp_comp_aktif'], state.tempCompActive),
|
||||
tempCompCoef: _toDouble(rt['temp_comp_koef'] ?? rt['tempCompKoef'] ?? 0.0, state.tempCompCoef),
|
||||
tempRefC: _toDouble(rt['temp_ref_c'] ?? rt['temp_ref'] ?? 0.0, state.tempRefC),
|
||||
otaTrigger: _toBool(rt['ota_trigger'], state.otaTrigger),
|
||||
relayAktif: _toBool(rt['selenoid'], state.relayAktif),
|
||||
historyCount: _toInt(rt['history_count'], state.historyCount),
|
||||
lastRealtime: _parseDateTime(rt['datetime']) ?? lastUpd,
|
||||
otaStatus: (rt['ota_status'] as String?) ?? state.otaStatus,
|
||||
));
|
||||
} else {
|
||||
// fallback: try single path
|
||||
|
|
@ -148,6 +169,16 @@ class EvaporasiSettingsBloc extends Bloc<EvaporasiSettingsEvent, EvaporasiSettin
|
|||
Emitter<EvaporasiSettingsState> emit,
|
||||
) => emit(state.copyWith(pumpEndTime: event.value));
|
||||
|
||||
void _onStandarTinggiChanged(
|
||||
EvaporasiStandarTinggiChanged event,
|
||||
Emitter<EvaporasiSettingsState> emit,
|
||||
) => emit(state.copyWith(standarTinggiCm: event.value));
|
||||
|
||||
void _onBatasKritisChanged(
|
||||
EvaporasiBatasKritisChanged event,
|
||||
Emitter<EvaporasiSettingsState> emit,
|
||||
) => emit(state.copyWith(batasKritisCm: event.value));
|
||||
|
||||
void _onD0Changed(
|
||||
EvaporasiD0Changed event,
|
||||
Emitter<EvaporasiSettingsState> emit,
|
||||
|
|
@ -188,6 +219,8 @@ class EvaporasiSettingsBloc extends Bloc<EvaporasiSettingsEvent, EvaporasiSettin
|
|||
|
||||
emit(state.copyWith(status: EvaporasiSettingsStatus.saving));
|
||||
try {
|
||||
final pumpStartHour = int.tryParse(state.pumpStartTime.split(':').first) ?? 0;
|
||||
final pumpEndHour = int.tryParse(state.pumpEndTime.split(':').first) ?? 0;
|
||||
await _ref.set({
|
||||
'threshold_rendah': state.thresholdRendah,
|
||||
'threshold_tinggi': state.thresholdTinggi,
|
||||
|
|
@ -195,8 +228,12 @@ class EvaporasiSettingsBloc extends Bloc<EvaporasiSettingsEvent, EvaporasiSettin
|
|||
'koreksi_offset': state.koreksiOffset,
|
||||
'pump_start_time': state.pumpStartTime,
|
||||
'pump_end_time': state.pumpEndTime,
|
||||
'jam_pompa_mulai': pumpStartHour,
|
||||
'jam_pompa_selesai': pumpEndHour,
|
||||
'd0': state.d0,
|
||||
'dmax_manual': state.dmaxManual,
|
||||
'standar_tinggi_cm': state.standarTinggiCm,
|
||||
'batas_kritis_cm': state.batasKritisCm,
|
||||
'interval_realtime_ms': state.intervalRealtime_ms,
|
||||
'interval_history_ms': state.intervalHistory_ms,
|
||||
'interval_baca_ms': state.intervalBaca_ms,
|
||||
|
|
@ -213,7 +250,43 @@ class EvaporasiSettingsBloc extends Bloc<EvaporasiSettingsEvent, EvaporasiSettin
|
|||
}
|
||||
}
|
||||
|
||||
double _toDouble(dynamic v, double def) => v == null ? def : (v as num).toDouble();
|
||||
double _toDouble(dynamic v, double def) {
|
||||
if (v == null) return def;
|
||||
if (v is num) return v.toDouble();
|
||||
if (v is String) {
|
||||
return double.tryParse(v) ?? def;
|
||||
}
|
||||
return def;
|
||||
}
|
||||
|
||||
int _toInt(dynamic v, int def) => v == null ? def : (v as num).toInt();
|
||||
int _toInt(dynamic v, int def) {
|
||||
if (v == null) return def;
|
||||
if (v is num) return v.toInt();
|
||||
if (v is String) {
|
||||
return int.tryParse(v) ?? def;
|
||||
}
|
||||
return def;
|
||||
}
|
||||
|
||||
bool _toBool(dynamic v, bool def) {
|
||||
if (v == null) return def;
|
||||
if (v is bool) return v;
|
||||
if (v is num) return v != 0;
|
||||
if (v is String) {
|
||||
final lower = v.toLowerCase().trim();
|
||||
return lower == 'true' || lower == '1' || lower == 'yes' || lower == 'aktif';
|
||||
}
|
||||
return def;
|
||||
}
|
||||
|
||||
DateTime? _parseDateTime(dynamic raw) {
|
||||
if (raw == null) return null;
|
||||
if (raw is int) return DateTime.fromMillisecondsSinceEpoch(raw).toLocal();
|
||||
if (raw is num) return DateTime.fromMillisecondsSinceEpoch(raw.toInt()).toLocal();
|
||||
if (raw is String) {
|
||||
final normalized = raw.replaceAll(' ', 'T');
|
||||
return DateTime.tryParse(normalized)?.toLocal();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,6 +63,18 @@ class EvaporasiPumpEndChanged extends EvaporasiSettingsEvent {
|
|||
@override List<Object?> get props => [value];
|
||||
}
|
||||
|
||||
class EvaporasiStandarTinggiChanged extends EvaporasiSettingsEvent {
|
||||
final double value;
|
||||
const EvaporasiStandarTinggiChanged(this.value);
|
||||
@override List<Object?> get props => [value];
|
||||
}
|
||||
|
||||
class EvaporasiBatasKritisChanged extends EvaporasiSettingsEvent {
|
||||
final double value;
|
||||
const EvaporasiBatasKritisChanged(this.value);
|
||||
@override List<Object?> get props => [value];
|
||||
}
|
||||
|
||||
class EvaporasiD0Changed extends EvaporasiSettingsEvent {
|
||||
final int value;
|
||||
const EvaporasiD0Changed(this.value);
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ class _EvaporasiSettingsScreenState extends State<EvaporasiSettingsScreen> {
|
|||
final _pumpEndController = TextEditingController();
|
||||
final _d0Controller = TextEditingController();
|
||||
final _dmaxManualController = TextEditingController();
|
||||
final _standarController = TextEditingController();
|
||||
final _batasController = TextEditingController();
|
||||
|
||||
void _syncControllers(EvaporasiSettingsState s) {
|
||||
final rendah = s.thresholdRendah.toStringAsFixed(1);
|
||||
|
|
@ -28,6 +30,8 @@ class _EvaporasiSettingsScreenState extends State<EvaporasiSettingsScreen> {
|
|||
final pumpEnd = s.pumpEndTime;
|
||||
final d0 = s.d0 == 0 ? '' : s.d0.toString();
|
||||
final dmaxManual = s.dmaxManual == 0 ? '' : s.dmaxManual.toString();
|
||||
final standar = s.standarTinggiCm.toStringAsFixed(1);
|
||||
final batas = s.batasKritisCm.toStringAsFixed(1);
|
||||
|
||||
if (_rendahController.text != rendah) {
|
||||
_rendahController.text = rendah;
|
||||
|
|
@ -50,6 +54,12 @@ class _EvaporasiSettingsScreenState extends State<EvaporasiSettingsScreen> {
|
|||
if (_dmaxManualController.text != dmaxManual) {
|
||||
_dmaxManualController.text = dmaxManual;
|
||||
}
|
||||
if (_standarController.text != standar) {
|
||||
_standarController.text = standar;
|
||||
}
|
||||
if (_batasController.text != batas) {
|
||||
_batasController.text = batas;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _pickPumpTime(BuildContext context, TextEditingController controller, ValueChanged<String> onSelected, String initialValue) async {
|
||||
|
|
@ -79,6 +89,8 @@ class _EvaporasiSettingsScreenState extends State<EvaporasiSettingsScreen> {
|
|||
_pumpEndController.dispose();
|
||||
_d0Controller.dispose();
|
||||
_dmaxManualController.dispose();
|
||||
_standarController.dispose();
|
||||
_batasController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
|
@ -142,6 +154,8 @@ class _EvaporasiSettingsScreenState extends State<EvaporasiSettingsScreen> {
|
|||
'langsung berlaku untuk semua perangkat.',
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
_groupTitle('Grup A — Sistem & Kalibrasi'),
|
||||
const SizedBox(height: 12),
|
||||
_sectionTitle('Batas Status Evaporasi'),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
|
|
@ -259,6 +273,8 @@ class _EvaporasiSettingsScreenState extends State<EvaporasiSettingsScreen> {
|
|||
),
|
||||
]),
|
||||
const SizedBox(height: 24),
|
||||
_groupTitle('Grup B — Aktuator & Pengaturan'),
|
||||
const SizedBox(height: 12),
|
||||
_sectionTitle('Kontrol Pompa'),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
|
|
@ -325,6 +341,32 @@ class _EvaporasiSettingsScreenState extends State<EvaporasiSettingsScreen> {
|
|||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_NumericField(
|
||||
controller: _standarController,
|
||||
label: 'Standar Tinggi Air (cm)',
|
||||
hint: 'Contoh: 18.0',
|
||||
helper: 'Nilai tinggi standar untuk selenoid (cm).',
|
||||
onChanged: (v) {
|
||||
final d = double.tryParse(v);
|
||||
if (d != null && d >= 0) {
|
||||
bloc.add(EvaporasiStandarTinggiChanged(d));
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_NumericField(
|
||||
controller: _batasController,
|
||||
label: 'Batas Kritis (cm)',
|
||||
hint: 'Contoh: 15.0',
|
||||
helper: 'Jika di bawah nilai ini → pompa ON paksa.',
|
||||
onChanged: (v) {
|
||||
final d = double.tryParse(v);
|
||||
if (d != null && d >= 0) {
|
||||
bloc.add(EvaporasiBatasKritisChanged(d));
|
||||
}
|
||||
},
|
||||
),
|
||||
]),
|
||||
const SizedBox(height: 24),
|
||||
SizedBox(
|
||||
|
|
@ -358,6 +400,11 @@ class _EvaporasiSettingsScreenState extends State<EvaporasiSettingsScreen> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _groupTitle(String text) => Text(
|
||||
text,
|
||||
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.bold, color: Colors.blueGrey),
|
||||
);
|
||||
|
||||
Widget _sectionTitle(String text) => Text(
|
||||
text,
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
||||
|
|
|
|||
|
|
@ -25,6 +25,20 @@ class EvaporasiSettingsState extends Equatable {
|
|||
final int intervalHistory_ms; // default 600000 = 10 menit
|
||||
final int intervalBaca_ms; // default 10000 = 10 detik
|
||||
|
||||
final double standarTinggiCm;
|
||||
final double batasKritisCm;
|
||||
final bool tempCompActive;
|
||||
final double tempCompCoef;
|
||||
final double tempRefC;
|
||||
final bool sensorError;
|
||||
final bool ntpSync;
|
||||
final double snapshotCm;
|
||||
final bool otaTrigger;
|
||||
final bool relayAktif;
|
||||
final String otaStatus;
|
||||
final int historyCount;
|
||||
final DateTime? lastRealtime;
|
||||
|
||||
final EvaporasiSettingsStatus status;
|
||||
final String? errorMessage;
|
||||
|
||||
|
|
@ -40,6 +54,19 @@ class EvaporasiSettingsState extends Equatable {
|
|||
this.intervalRealtime_ms = 300000,
|
||||
this.intervalHistory_ms = 600000,
|
||||
this.intervalBaca_ms = 10000,
|
||||
this.standarTinggiCm = 18.0,
|
||||
this.batasKritisCm = 15.0,
|
||||
this.tempCompActive = true,
|
||||
this.tempCompCoef = 500.0,
|
||||
this.tempRefC = 25.0,
|
||||
this.sensorError = false,
|
||||
this.ntpSync = false,
|
||||
this.snapshotCm = 0.0,
|
||||
this.otaTrigger = false,
|
||||
this.relayAktif = false,
|
||||
this.otaStatus = '--',
|
||||
this.historyCount = 0,
|
||||
this.lastRealtime,
|
||||
this.status = EvaporasiSettingsStatus.loading,
|
||||
this.errorMessage,
|
||||
this.dmax = 0,
|
||||
|
|
@ -64,6 +91,19 @@ class EvaporasiSettingsState extends Equatable {
|
|||
int? intervalRealtime_ms,
|
||||
int? intervalHistory_ms,
|
||||
int? intervalBaca_ms,
|
||||
double? standarTinggiCm,
|
||||
double? batasKritisCm,
|
||||
bool? tempCompActive,
|
||||
double? tempCompCoef,
|
||||
double? tempRefC,
|
||||
bool? sensorError,
|
||||
bool? ntpSync,
|
||||
double? snapshotCm,
|
||||
bool? otaTrigger,
|
||||
bool? relayAktif,
|
||||
String? otaStatus,
|
||||
int? historyCount,
|
||||
DateTime? lastRealtime,
|
||||
EvaporasiSettingsStatus? status,
|
||||
String? errorMessage,
|
||||
int? dmax,
|
||||
|
|
@ -84,6 +124,19 @@ class EvaporasiSettingsState extends Equatable {
|
|||
pumpEndTime: pumpEndTime ?? this.pumpEndTime,
|
||||
d0: d0 ?? this.d0,
|
||||
dmaxManual: dmaxManual ?? this.dmaxManual,
|
||||
standarTinggiCm: standarTinggiCm ?? this.standarTinggiCm,
|
||||
batasKritisCm: batasKritisCm ?? this.batasKritisCm,
|
||||
tempCompActive: tempCompActive ?? this.tempCompActive,
|
||||
tempCompCoef: tempCompCoef ?? this.tempCompCoef,
|
||||
tempRefC: tempRefC ?? this.tempRefC,
|
||||
sensorError: sensorError ?? this.sensorError,
|
||||
ntpSync: ntpSync ?? this.ntpSync,
|
||||
snapshotCm: snapshotCm ?? this.snapshotCm,
|
||||
otaTrigger: otaTrigger ?? this.otaTrigger,
|
||||
relayAktif: relayAktif ?? this.relayAktif,
|
||||
otaStatus: otaStatus ?? this.otaStatus,
|
||||
historyCount: historyCount ?? this.historyCount,
|
||||
lastRealtime: lastRealtime ?? this.lastRealtime,
|
||||
firmwareVersion: firmwareVersion ?? this.firmwareVersion,
|
||||
wifiConnected: wifiConnected ?? this.wifiConnected,
|
||||
firebaseConnected: firebaseConnected ?? this.firebaseConnected,
|
||||
|
|
@ -104,8 +157,10 @@ class EvaporasiSettingsState extends Equatable {
|
|||
List<Object?> get props => [
|
||||
thresholdRendah, thresholdTinggi, rumusKalibrasi, koreksiOffset,
|
||||
pumpStartTime, pumpEndTime, d0, dmaxManual,
|
||||
firmwareVersion, wifiConnected, firebaseConnected, activeD0, activeDmax, lastUpdate,
|
||||
intervalRealtime_ms, intervalHistory_ms, intervalBaca_ms,
|
||||
standarTinggiCm, batasKritisCm, tempCompActive, tempCompCoef, tempRefC,
|
||||
sensorError, ntpSync, snapshotCm, otaTrigger, relayAktif, historyCount, lastRealtime,
|
||||
firmwareVersion, wifiConnected, firebaseConnected, activeD0, activeDmax, lastUpdate,
|
||||
status, errorMessage,
|
||||
dmax, isResettingDmax,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -599,6 +599,66 @@ class _EvaporasiScreenState extends State<EvaporasiScreen> {
|
|||
);
|
||||
}
|
||||
|
||||
Widget _detailCard(
|
||||
double width,
|
||||
String title,
|
||||
String value,
|
||||
IconData icon,
|
||||
Color color, {
|
||||
String? subtitle,
|
||||
}) {
|
||||
return Container(
|
||||
width: width,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(icon, color: color, size: 20),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade700,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Widget _statusCard(EvaporasiState state) {
|
||||
Color statusColor;
|
||||
IconData statusIcon;
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ class _EvaporasiControlPanelState extends State<EvaporasiControlPanel> {
|
|||
bool _selenoid = false;
|
||||
bool _isTogglingSelenoid = false;
|
||||
bool _isResettingEvaporasi = false;
|
||||
bool _isTriggeringOta = false;
|
||||
StreamSubscription<DatabaseEvent>? _subscription;
|
||||
|
||||
@override
|
||||
|
|
@ -127,6 +128,42 @@ class _EvaporasiControlPanelState extends State<EvaporasiControlPanel> {
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _triggerOTA() async {
|
||||
if (_isTriggeringOta) return;
|
||||
setState(() {
|
||||
_isTriggeringOta = true;
|
||||
});
|
||||
|
||||
try {
|
||||
await Future.wait([
|
||||
FirebaseDatabase.instance.ref('Monitoring/ota_trigger').set(true),
|
||||
FirebaseDatabase.instance
|
||||
.ref('Monitoring/realtime/ota_trigger')
|
||||
.set(true),
|
||||
]);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||
content: Text('Perintah OTA trigger berhasil dikirim.'),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
));
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content: Text('Gagal mengirim perintah OTA trigger: $e'),
|
||||
backgroundColor: Colors.red.shade600,
|
||||
behavior: SnackBarBehavior.floating,
|
||||
));
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isTriggeringOta = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_subscription?.cancel();
|
||||
|
|
@ -160,7 +197,7 @@ class _EvaporasiControlPanelState extends State<EvaporasiControlPanel> {
|
|||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Tombol ini terhubung ke path Firebase yang belum ada di UI utama: reset evaporasi dan kontrol selenoid.',
|
||||
'Tombol ini terhubung ke Firebase untuk reset evaporasi, kontrol selenoid, dan trigger OTA.',
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
|
@ -213,6 +250,24 @@ class _EvaporasiControlPanelState extends State<EvaporasiControlPanel> {
|
|||
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
onPressed: _isTriggeringOta ? null : _triggerOTA,
|
||||
icon: _isTriggeringOta
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2, color: Colors.white),
|
||||
)
|
||||
: const Icon(Icons.cloud_upload_rounded),
|
||||
label: Text(_isTriggeringOta ? 'Memanggil OTA...' : 'Trigger OTA'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blue.shade700,
|
||||
foregroundColor: Colors.white,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
|
|
|||
|
|
@ -7,6 +7,20 @@ class Evaporasi {
|
|||
final double acuanPagi;
|
||||
final String status;
|
||||
final DateTime timestamp;
|
||||
final bool sensorError;
|
||||
final bool ntpSync;
|
||||
final double snapshotCm;
|
||||
final double standarTinggi;
|
||||
final double batasKritis;
|
||||
final int jamPompaMulai;
|
||||
final int jamPompaSelesai;
|
||||
final bool tempCompActive;
|
||||
final double tempCompCoef;
|
||||
final double tempRefC;
|
||||
final String otaVersion;
|
||||
final String otaStatus;
|
||||
final bool otaTrigger;
|
||||
final bool relayAktif;
|
||||
|
||||
Evaporasi({
|
||||
required this.evaporasi,
|
||||
|
|
@ -15,6 +29,20 @@ class Evaporasi {
|
|||
required this.acuanPagi,
|
||||
required this.status,
|
||||
required this.timestamp,
|
||||
this.sensorError = false,
|
||||
this.ntpSync = false,
|
||||
this.snapshotCm = 0.0,
|
||||
this.standarTinggi = 0.0,
|
||||
this.batasKritis = 0.0,
|
||||
this.jamPompaMulai = 0,
|
||||
this.jamPompaSelesai = 0,
|
||||
this.tempCompActive = false,
|
||||
this.tempCompCoef = 0.0,
|
||||
this.tempRefC = 0.0,
|
||||
this.otaVersion = '-',
|
||||
this.otaStatus = '-',
|
||||
this.otaTrigger = false,
|
||||
this.relayAktif = false,
|
||||
});
|
||||
|
||||
static final empty = Evaporasi(
|
||||
|
|
@ -41,6 +69,30 @@ class Evaporasi {
|
|||
return 0.0;
|
||||
}
|
||||
|
||||
bool toBoolSafe(dynamic v) {
|
||||
if (v is bool) return v;
|
||||
if (v is num) return v != 0;
|
||||
if (v is String) {
|
||||
final lower = v.trim().toLowerCase();
|
||||
return lower == 'true' || lower == '1' || lower == 'yes' || lower == 'aktif';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int toIntSafe(dynamic v) {
|
||||
if (v is int) return v;
|
||||
if (v is num) return v.toInt();
|
||||
if (v is String) {
|
||||
return int.tryParse(v.trim()) ?? 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
String toStringSafe(dynamic v) {
|
||||
if (v == null) return '-';
|
||||
return v.toString();
|
||||
}
|
||||
|
||||
// ── Evaporasi (mm) ───────────────────────────────────
|
||||
final evaporasiRaw = toDoubleSafe(
|
||||
json['evaporasi_mm'] ??
|
||||
|
|
@ -51,13 +103,11 @@ class Evaporasi {
|
|||
json['evaporasi_value'] ??
|
||||
json['evaporasi_k'],
|
||||
);
|
||||
// Sudah dalam mm, tidak perlu konversi
|
||||
final evaporasiVal = evaporasiRaw;
|
||||
|
||||
// ── Suhu (°C) ────────────────────────────────────────
|
||||
// FIX: tambah 'suhu_air_c' sesuai field yang dikirim ESP32
|
||||
final suhuRaw = toDoubleSafe(
|
||||
json['suhu_air_c'] ?? // ← ESP32 kirim field ini
|
||||
json['suhu_air_c'] ??
|
||||
json['suhu_air'] ??
|
||||
json['suhu'] ??
|
||||
json['suhuAir'] ??
|
||||
|
|
@ -77,7 +127,6 @@ class Evaporasi {
|
|||
json['tinggi_air_m'] ??
|
||||
json['tinggiAir_m'],
|
||||
);
|
||||
// Sudah dalam cm, tidak perlu konversi
|
||||
final tinggiVal = tinggiRaw;
|
||||
|
||||
// ── Sanity check ─────────────────────────────────────
|
||||
|
|
@ -92,9 +141,79 @@ class Evaporasi {
|
|||
// ── Status ───────────────────────────────────────────
|
||||
final statusVal = json['status']?.toString() ?? "Normal";
|
||||
|
||||
// ── Parse Timestamp ──────────────────────────────────
|
||||
// FIX: Tambahkan offset +07:00 (WIB) jika string tidak punya info timezone,
|
||||
// agar tidak terjadi mismatch 7 jam antara Firebase dan Flutter.
|
||||
final sensorErrorVal = toBoolSafe(
|
||||
json['sensor_error'] ??
|
||||
json['sensorError'] ??
|
||||
json['sensor'] ??
|
||||
json['status_sensor'],
|
||||
);
|
||||
final ntpSyncVal = toBoolSafe(
|
||||
json['ntp_sync'] ??
|
||||
json['ntpSync'] ??
|
||||
json['ntp'],
|
||||
);
|
||||
final snapshotVal = toDoubleSafe(
|
||||
json['snapshot_cm'] ??
|
||||
json['snapshot'] ??
|
||||
json['acuan_pagi_cm'] ??
|
||||
0.0,
|
||||
);
|
||||
final standarTinggiVal = toDoubleSafe(
|
||||
json['standar_tinggi'] ??
|
||||
json['standarTinggi'] ??
|
||||
json['standard_height'] ??
|
||||
0.0,
|
||||
);
|
||||
final batasKritisVal = toDoubleSafe(
|
||||
json['batas_kritis'] ??
|
||||
json['batasKritis'] ??
|
||||
json['critical_level'] ??
|
||||
0.0,
|
||||
);
|
||||
final jamPompaMulaiVal = toIntSafe(
|
||||
json['jam_pompa_mulai'] ??
|
||||
json['jamPompaMulai'] ??
|
||||
json['pump_start'] ??
|
||||
0,
|
||||
);
|
||||
final jamPompaSelesaiVal = toIntSafe(
|
||||
json['jam_pompa_selesai'] ??
|
||||
json['jamPompaSelesai'] ??
|
||||
json['pump_end'] ??
|
||||
0,
|
||||
);
|
||||
final tempCompActiveVal = toBoolSafe(
|
||||
json['temp_comp_aktif'] ??
|
||||
json['temp_comp_aktif'] ??
|
||||
json['tempCompAktif'] ??
|
||||
json['temp_comp_active'] ??
|
||||
false,
|
||||
);
|
||||
final tempCompCoefVal = toDoubleSafe(
|
||||
json['temp_comp_koef'] ??
|
||||
json['tempCompKoef'] ??
|
||||
json['temp_comp_coefficient'] ??
|
||||
0.0,
|
||||
);
|
||||
final tempRefCVal = toDoubleSafe(
|
||||
json['temp_ref_c'] ??
|
||||
json['tempRefC'] ??
|
||||
json['temp_ref'] ??
|
||||
0.0,
|
||||
);
|
||||
final otaVersionVal = toStringSafe(
|
||||
json['ota_version'] ?? json['otaVersion'] ?? json['firmware_version'] ?? '-',
|
||||
);
|
||||
final otaStatusVal = toStringSafe(
|
||||
json['ota_status'] ?? json['otaStatus'] ?? '-',
|
||||
);
|
||||
final otaTriggerVal = toBoolSafe(
|
||||
json['ota_trigger'] ?? json['otaTrigger'] ?? false,
|
||||
);
|
||||
final relayAktifVal = toBoolSafe(
|
||||
json['selenoid'] ?? json['selenoid_on'] ?? json['relay'],
|
||||
);
|
||||
|
||||
DateTime parseTimestamp(dynamic rawTimestamp) {
|
||||
try {
|
||||
if (rawTimestamp is int) {
|
||||
|
|
@ -117,7 +236,6 @@ class Evaporasi {
|
|||
if (rawTimestamp is String) {
|
||||
String s = rawTimestamp.trim();
|
||||
|
||||
// UNIX string
|
||||
final unixValue = int.tryParse(s);
|
||||
if (unixValue != null) {
|
||||
if (unixValue < 1000000000000) {
|
||||
|
|
@ -128,13 +246,10 @@ class Evaporasi {
|
|||
return DateTime.fromMillisecondsSinceEpoch(unixValue).toLocal();
|
||||
}
|
||||
|
||||
// Format "YYYY-MM-DD HH:mm:ss" → tambah 'T' agar bisa diparsing
|
||||
if (s.contains(' ') && !s.contains('T')) {
|
||||
s = s.replaceFirst(' ', 'T');
|
||||
}
|
||||
|
||||
// FIX: Jika tidak ada info timezone, anggap WIB (UTC+7)
|
||||
// agar jam di chart tidak mismatch 7 jam
|
||||
if (!s.contains('+') && !s.contains('Z') && !s.contains('-', 10)) {
|
||||
s = '${s}+07:00';
|
||||
}
|
||||
|
|
@ -143,18 +258,15 @@ class Evaporasi {
|
|||
if (parsed != null) return parsed.toLocal();
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
return DateTime.fromMillisecondsSinceEpoch(0);
|
||||
}
|
||||
|
||||
DateTime timestamp = DateTime.fromMillisecondsSinceEpoch(0);
|
||||
|
||||
final rawTimestamp = json['timestamp'] ?? json['time'] ?? json['datetime'];
|
||||
|
||||
if (rawTimestamp != null) {
|
||||
timestamp = parseTimestamp(rawTimestamp);
|
||||
} else {
|
||||
// Legacy fallback: field "waktu" format "HH:mm:ss"
|
||||
final waktuStr = json['waktu'] as String?;
|
||||
if (waktuStr != null) {
|
||||
final parts = waktuStr.split(':');
|
||||
|
|
@ -175,6 +287,20 @@ class Evaporasi {
|
|||
acuanPagi: acuanPagiVal,
|
||||
status: statusVal,
|
||||
timestamp: timestamp,
|
||||
sensorError: sensorErrorVal,
|
||||
ntpSync: ntpSyncVal,
|
||||
snapshotCm: snapshotVal,
|
||||
standarTinggi: standarTinggiVal,
|
||||
batasKritis: batasKritisVal,
|
||||
jamPompaMulai: jamPompaMulaiVal,
|
||||
jamPompaSelesai: jamPompaSelesaiVal,
|
||||
tempCompActive: tempCompActiveVal,
|
||||
tempCompCoef: tempCompCoefVal,
|
||||
tempRefC: tempRefCVal,
|
||||
otaVersion: otaVersionVal,
|
||||
otaStatus: otaStatusVal,
|
||||
otaTrigger: otaTriggerVal,
|
||||
relayAktif: relayAktifVal,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue