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<EvaporasiKoreksiOffsetChanged>(_onOffsetChanged);
|
||||||
on<EvaporasiPumpStartChanged>(_onPumpStartChanged);
|
on<EvaporasiPumpStartChanged>(_onPumpStartChanged);
|
||||||
on<EvaporasiPumpEndChanged>(_onPumpEndChanged);
|
on<EvaporasiPumpEndChanged>(_onPumpEndChanged);
|
||||||
|
on<EvaporasiStandarTinggiChanged>(_onStandarTinggiChanged);
|
||||||
|
on<EvaporasiBatasKritisChanged>(_onBatasKritisChanged);
|
||||||
on<EvaporasiD0Changed>(_onD0Changed);
|
on<EvaporasiD0Changed>(_onD0Changed);
|
||||||
on<EvaporasiDmaxManualChanged>(_onDmaxManualChanged);
|
on<EvaporasiDmaxManualChanged>(_onDmaxManualChanged);
|
||||||
on<EvaporasiIntervalRealtimeChanged>(_onIntervalRealtimeChanged);
|
on<EvaporasiIntervalRealtimeChanged>(_onIntervalRealtimeChanged);
|
||||||
|
|
@ -44,10 +46,16 @@ class EvaporasiSettingsBloc extends Bloc<EvaporasiSettingsEvent, EvaporasiSettin
|
||||||
thresholdRendah: _toDouble(data['threshold_rendah'], 2.0),
|
thresholdRendah: _toDouble(data['threshold_rendah'], 2.0),
|
||||||
thresholdTinggi: _toDouble(data['threshold_tinggi'], 10.0),
|
thresholdTinggi: _toDouble(data['threshold_tinggi'], 10.0),
|
||||||
rumusKalibrasi: (data['rumus_kalibrasi'] as String?) ?? 'selisih_max',
|
rumusKalibrasi: (data['rumus_kalibrasi'] as String?) ?? 'selisih_max',
|
||||||
koreksiOffset: _toDouble(data['koreksi_offset'], 0.0), pumpStartTime: (data['pump_start_time'] as String?) ?? '06:00',
|
koreksiOffset: _toDouble(data['koreksi_offset'], 0.0),
|
||||||
pumpEndTime: (data['pump_end_time'] as String?) ?? '18:00',
|
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),
|
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),
|
intervalHistory_ms: _toInt(data['interval_history_ms'], 600000),
|
||||||
intervalBaca_ms: _toInt(data['interval_baca_ms'], 10000),
|
intervalBaca_ms: _toInt(data['interval_baca_ms'], 10000),
|
||||||
status: EvaporasiSettingsStatus.loaded,
|
status: EvaporasiSettingsStatus.loaded,
|
||||||
|
|
@ -91,6 +99,19 @@ class EvaporasiSettingsBloc extends Bloc<EvaporasiSettingsEvent, EvaporasiSettin
|
||||||
activeD0: activeD0,
|
activeD0: activeD0,
|
||||||
activeDmax: activeDmax,
|
activeDmax: activeDmax,
|
||||||
lastUpdate: lastUpd,
|
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 {
|
} else {
|
||||||
// fallback: try single path
|
// fallback: try single path
|
||||||
|
|
@ -148,6 +169,16 @@ class EvaporasiSettingsBloc extends Bloc<EvaporasiSettingsEvent, EvaporasiSettin
|
||||||
Emitter<EvaporasiSettingsState> emit,
|
Emitter<EvaporasiSettingsState> emit,
|
||||||
) => emit(state.copyWith(pumpEndTime: event.value));
|
) => 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(
|
void _onD0Changed(
|
||||||
EvaporasiD0Changed event,
|
EvaporasiD0Changed event,
|
||||||
Emitter<EvaporasiSettingsState> emit,
|
Emitter<EvaporasiSettingsState> emit,
|
||||||
|
|
@ -188,6 +219,8 @@ class EvaporasiSettingsBloc extends Bloc<EvaporasiSettingsEvent, EvaporasiSettin
|
||||||
|
|
||||||
emit(state.copyWith(status: EvaporasiSettingsStatus.saving));
|
emit(state.copyWith(status: EvaporasiSettingsStatus.saving));
|
||||||
try {
|
try {
|
||||||
|
final pumpStartHour = int.tryParse(state.pumpStartTime.split(':').first) ?? 0;
|
||||||
|
final pumpEndHour = int.tryParse(state.pumpEndTime.split(':').first) ?? 0;
|
||||||
await _ref.set({
|
await _ref.set({
|
||||||
'threshold_rendah': state.thresholdRendah,
|
'threshold_rendah': state.thresholdRendah,
|
||||||
'threshold_tinggi': state.thresholdTinggi,
|
'threshold_tinggi': state.thresholdTinggi,
|
||||||
|
|
@ -195,8 +228,12 @@ class EvaporasiSettingsBloc extends Bloc<EvaporasiSettingsEvent, EvaporasiSettin
|
||||||
'koreksi_offset': state.koreksiOffset,
|
'koreksi_offset': state.koreksiOffset,
|
||||||
'pump_start_time': state.pumpStartTime,
|
'pump_start_time': state.pumpStartTime,
|
||||||
'pump_end_time': state.pumpEndTime,
|
'pump_end_time': state.pumpEndTime,
|
||||||
|
'jam_pompa_mulai': pumpStartHour,
|
||||||
|
'jam_pompa_selesai': pumpEndHour,
|
||||||
'd0': state.d0,
|
'd0': state.d0,
|
||||||
'dmax_manual': state.dmaxManual,
|
'dmax_manual': state.dmaxManual,
|
||||||
|
'standar_tinggi_cm': state.standarTinggiCm,
|
||||||
|
'batas_kritis_cm': state.batasKritisCm,
|
||||||
'interval_realtime_ms': state.intervalRealtime_ms,
|
'interval_realtime_ms': state.intervalRealtime_ms,
|
||||||
'interval_history_ms': state.intervalHistory_ms,
|
'interval_history_ms': state.intervalHistory_ms,
|
||||||
'interval_baca_ms': state.intervalBaca_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];
|
@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 {
|
class EvaporasiD0Changed extends EvaporasiSettingsEvent {
|
||||||
final int value;
|
final int value;
|
||||||
const EvaporasiD0Changed(this.value);
|
const EvaporasiD0Changed(this.value);
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ class _EvaporasiSettingsScreenState extends State<EvaporasiSettingsScreen> {
|
||||||
final _pumpEndController = TextEditingController();
|
final _pumpEndController = TextEditingController();
|
||||||
final _d0Controller = TextEditingController();
|
final _d0Controller = TextEditingController();
|
||||||
final _dmaxManualController = TextEditingController();
|
final _dmaxManualController = TextEditingController();
|
||||||
|
final _standarController = TextEditingController();
|
||||||
|
final _batasController = TextEditingController();
|
||||||
|
|
||||||
void _syncControllers(EvaporasiSettingsState s) {
|
void _syncControllers(EvaporasiSettingsState s) {
|
||||||
final rendah = s.thresholdRendah.toStringAsFixed(1);
|
final rendah = s.thresholdRendah.toStringAsFixed(1);
|
||||||
|
|
@ -28,6 +30,8 @@ class _EvaporasiSettingsScreenState extends State<EvaporasiSettingsScreen> {
|
||||||
final pumpEnd = s.pumpEndTime;
|
final pumpEnd = s.pumpEndTime;
|
||||||
final d0 = s.d0 == 0 ? '' : s.d0.toString();
|
final d0 = s.d0 == 0 ? '' : s.d0.toString();
|
||||||
final dmaxManual = s.dmaxManual == 0 ? '' : s.dmaxManual.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) {
|
if (_rendahController.text != rendah) {
|
||||||
_rendahController.text = rendah;
|
_rendahController.text = rendah;
|
||||||
|
|
@ -50,6 +54,12 @@ class _EvaporasiSettingsScreenState extends State<EvaporasiSettingsScreen> {
|
||||||
if (_dmaxManualController.text != dmaxManual) {
|
if (_dmaxManualController.text != dmaxManual) {
|
||||||
_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 {
|
Future<void> _pickPumpTime(BuildContext context, TextEditingController controller, ValueChanged<String> onSelected, String initialValue) async {
|
||||||
|
|
@ -79,6 +89,8 @@ class _EvaporasiSettingsScreenState extends State<EvaporasiSettingsScreen> {
|
||||||
_pumpEndController.dispose();
|
_pumpEndController.dispose();
|
||||||
_d0Controller.dispose();
|
_d0Controller.dispose();
|
||||||
_dmaxManualController.dispose();
|
_dmaxManualController.dispose();
|
||||||
|
_standarController.dispose();
|
||||||
|
_batasController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -142,6 +154,8 @@ class _EvaporasiSettingsScreenState extends State<EvaporasiSettingsScreen> {
|
||||||
'langsung berlaku untuk semua perangkat.',
|
'langsung berlaku untuk semua perangkat.',
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
_groupTitle('Grup A — Sistem & Kalibrasi'),
|
||||||
|
const SizedBox(height: 12),
|
||||||
_sectionTitle('Batas Status Evaporasi'),
|
_sectionTitle('Batas Status Evaporasi'),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Text(
|
Text(
|
||||||
|
|
@ -259,6 +273,8 @@ class _EvaporasiSettingsScreenState extends State<EvaporasiSettingsScreen> {
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
_groupTitle('Grup B — Aktuator & Pengaturan'),
|
||||||
|
const SizedBox(height: 12),
|
||||||
_sectionTitle('Kontrol Pompa'),
|
_sectionTitle('Kontrol Pompa'),
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Text(
|
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),
|
const SizedBox(height: 24),
|
||||||
SizedBox(
|
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(
|
Widget _sectionTitle(String text) => Text(
|
||||||
text,
|
text,
|
||||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
|
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 intervalHistory_ms; // default 600000 = 10 menit
|
||||||
final int intervalBaca_ms; // default 10000 = 10 detik
|
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 EvaporasiSettingsStatus status;
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
|
|
||||||
|
|
@ -40,6 +54,19 @@ class EvaporasiSettingsState extends Equatable {
|
||||||
this.intervalRealtime_ms = 300000,
|
this.intervalRealtime_ms = 300000,
|
||||||
this.intervalHistory_ms = 600000,
|
this.intervalHistory_ms = 600000,
|
||||||
this.intervalBaca_ms = 10000,
|
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.status = EvaporasiSettingsStatus.loading,
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
this.dmax = 0,
|
this.dmax = 0,
|
||||||
|
|
@ -64,6 +91,19 @@ class EvaporasiSettingsState extends Equatable {
|
||||||
int? intervalRealtime_ms,
|
int? intervalRealtime_ms,
|
||||||
int? intervalHistory_ms,
|
int? intervalHistory_ms,
|
||||||
int? intervalBaca_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,
|
EvaporasiSettingsStatus? status,
|
||||||
String? errorMessage,
|
String? errorMessage,
|
||||||
int? dmax,
|
int? dmax,
|
||||||
|
|
@ -84,6 +124,19 @@ class EvaporasiSettingsState extends Equatable {
|
||||||
pumpEndTime: pumpEndTime ?? this.pumpEndTime,
|
pumpEndTime: pumpEndTime ?? this.pumpEndTime,
|
||||||
d0: d0 ?? this.d0,
|
d0: d0 ?? this.d0,
|
||||||
dmaxManual: dmaxManual ?? this.dmaxManual,
|
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,
|
firmwareVersion: firmwareVersion ?? this.firmwareVersion,
|
||||||
wifiConnected: wifiConnected ?? this.wifiConnected,
|
wifiConnected: wifiConnected ?? this.wifiConnected,
|
||||||
firebaseConnected: firebaseConnected ?? this.firebaseConnected,
|
firebaseConnected: firebaseConnected ?? this.firebaseConnected,
|
||||||
|
|
@ -104,8 +157,10 @@ class EvaporasiSettingsState extends Equatable {
|
||||||
List<Object?> get props => [
|
List<Object?> get props => [
|
||||||
thresholdRendah, thresholdTinggi, rumusKalibrasi, koreksiOffset,
|
thresholdRendah, thresholdTinggi, rumusKalibrasi, koreksiOffset,
|
||||||
pumpStartTime, pumpEndTime, d0, dmaxManual,
|
pumpStartTime, pumpEndTime, d0, dmaxManual,
|
||||||
firmwareVersion, wifiConnected, firebaseConnected, activeD0, activeDmax, lastUpdate,
|
|
||||||
intervalRealtime_ms, intervalHistory_ms, intervalBaca_ms,
|
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,
|
status, errorMessage,
|
||||||
dmax, isResettingDmax,
|
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) {
|
Widget _statusCard(EvaporasiState state) {
|
||||||
Color statusColor;
|
Color statusColor;
|
||||||
IconData statusIcon;
|
IconData statusIcon;
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ class _EvaporasiControlPanelState extends State<EvaporasiControlPanel> {
|
||||||
bool _selenoid = false;
|
bool _selenoid = false;
|
||||||
bool _isTogglingSelenoid = false;
|
bool _isTogglingSelenoid = false;
|
||||||
bool _isResettingEvaporasi = false;
|
bool _isResettingEvaporasi = false;
|
||||||
|
bool _isTriggeringOta = false;
|
||||||
StreamSubscription<DatabaseEvent>? _subscription;
|
StreamSubscription<DatabaseEvent>? _subscription;
|
||||||
|
|
||||||
@override
|
@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
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_subscription?.cancel();
|
_subscription?.cancel();
|
||||||
|
|
@ -160,7 +197,7 @@ class _EvaporasiControlPanelState extends State<EvaporasiControlPanel> {
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text(
|
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),
|
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
@ -213,6 +250,24 @@ class _EvaporasiControlPanelState extends State<EvaporasiControlPanel> {
|
||||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
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),
|
const SizedBox(height: 16),
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,20 @@ class Evaporasi {
|
||||||
final double acuanPagi;
|
final double acuanPagi;
|
||||||
final String status;
|
final String status;
|
||||||
final DateTime timestamp;
|
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({
|
Evaporasi({
|
||||||
required this.evaporasi,
|
required this.evaporasi,
|
||||||
|
|
@ -15,6 +29,20 @@ class Evaporasi {
|
||||||
required this.acuanPagi,
|
required this.acuanPagi,
|
||||||
required this.status,
|
required this.status,
|
||||||
required this.timestamp,
|
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(
|
static final empty = Evaporasi(
|
||||||
|
|
@ -41,6 +69,30 @@ class Evaporasi {
|
||||||
return 0.0;
|
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) ───────────────────────────────────
|
// ── Evaporasi (mm) ───────────────────────────────────
|
||||||
final evaporasiRaw = toDoubleSafe(
|
final evaporasiRaw = toDoubleSafe(
|
||||||
json['evaporasi_mm'] ??
|
json['evaporasi_mm'] ??
|
||||||
|
|
@ -51,13 +103,11 @@ class Evaporasi {
|
||||||
json['evaporasi_value'] ??
|
json['evaporasi_value'] ??
|
||||||
json['evaporasi_k'],
|
json['evaporasi_k'],
|
||||||
);
|
);
|
||||||
// Sudah dalam mm, tidak perlu konversi
|
|
||||||
final evaporasiVal = evaporasiRaw;
|
final evaporasiVal = evaporasiRaw;
|
||||||
|
|
||||||
// ── Suhu (°C) ────────────────────────────────────────
|
// ── Suhu (°C) ────────────────────────────────────────
|
||||||
// FIX: tambah 'suhu_air_c' sesuai field yang dikirim ESP32
|
|
||||||
final suhuRaw = toDoubleSafe(
|
final suhuRaw = toDoubleSafe(
|
||||||
json['suhu_air_c'] ?? // ← ESP32 kirim field ini
|
json['suhu_air_c'] ??
|
||||||
json['suhu_air'] ??
|
json['suhu_air'] ??
|
||||||
json['suhu'] ??
|
json['suhu'] ??
|
||||||
json['suhuAir'] ??
|
json['suhuAir'] ??
|
||||||
|
|
@ -77,7 +127,6 @@ class Evaporasi {
|
||||||
json['tinggi_air_m'] ??
|
json['tinggi_air_m'] ??
|
||||||
json['tinggiAir_m'],
|
json['tinggiAir_m'],
|
||||||
);
|
);
|
||||||
// Sudah dalam cm, tidak perlu konversi
|
|
||||||
final tinggiVal = tinggiRaw;
|
final tinggiVal = tinggiRaw;
|
||||||
|
|
||||||
// ── Sanity check ─────────────────────────────────────
|
// ── Sanity check ─────────────────────────────────────
|
||||||
|
|
@ -92,9 +141,79 @@ class Evaporasi {
|
||||||
// ── Status ───────────────────────────────────────────
|
// ── Status ───────────────────────────────────────────
|
||||||
final statusVal = json['status']?.toString() ?? "Normal";
|
final statusVal = json['status']?.toString() ?? "Normal";
|
||||||
|
|
||||||
// ── Parse Timestamp ──────────────────────────────────
|
final sensorErrorVal = toBoolSafe(
|
||||||
// FIX: Tambahkan offset +07:00 (WIB) jika string tidak punya info timezone,
|
json['sensor_error'] ??
|
||||||
// agar tidak terjadi mismatch 7 jam antara Firebase dan Flutter.
|
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) {
|
DateTime parseTimestamp(dynamic rawTimestamp) {
|
||||||
try {
|
try {
|
||||||
if (rawTimestamp is int) {
|
if (rawTimestamp is int) {
|
||||||
|
|
@ -117,7 +236,6 @@ class Evaporasi {
|
||||||
if (rawTimestamp is String) {
|
if (rawTimestamp is String) {
|
||||||
String s = rawTimestamp.trim();
|
String s = rawTimestamp.trim();
|
||||||
|
|
||||||
// UNIX string
|
|
||||||
final unixValue = int.tryParse(s);
|
final unixValue = int.tryParse(s);
|
||||||
if (unixValue != null) {
|
if (unixValue != null) {
|
||||||
if (unixValue < 1000000000000) {
|
if (unixValue < 1000000000000) {
|
||||||
|
|
@ -128,13 +246,10 @@ class Evaporasi {
|
||||||
return DateTime.fromMillisecondsSinceEpoch(unixValue).toLocal();
|
return DateTime.fromMillisecondsSinceEpoch(unixValue).toLocal();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format "YYYY-MM-DD HH:mm:ss" → tambah 'T' agar bisa diparsing
|
|
||||||
if (s.contains(' ') && !s.contains('T')) {
|
if (s.contains(' ') && !s.contains('T')) {
|
||||||
s = s.replaceFirst(' ', '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)) {
|
if (!s.contains('+') && !s.contains('Z') && !s.contains('-', 10)) {
|
||||||
s = '${s}+07:00';
|
s = '${s}+07:00';
|
||||||
}
|
}
|
||||||
|
|
@ -143,18 +258,15 @@ class Evaporasi {
|
||||||
if (parsed != null) return parsed.toLocal();
|
if (parsed != null) return parsed.toLocal();
|
||||||
}
|
}
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
return DateTime.fromMillisecondsSinceEpoch(0);
|
return DateTime.fromMillisecondsSinceEpoch(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
DateTime timestamp = DateTime.fromMillisecondsSinceEpoch(0);
|
DateTime timestamp = DateTime.fromMillisecondsSinceEpoch(0);
|
||||||
|
|
||||||
final rawTimestamp = json['timestamp'] ?? json['time'] ?? json['datetime'];
|
final rawTimestamp = json['timestamp'] ?? json['time'] ?? json['datetime'];
|
||||||
|
|
||||||
if (rawTimestamp != null) {
|
if (rawTimestamp != null) {
|
||||||
timestamp = parseTimestamp(rawTimestamp);
|
timestamp = parseTimestamp(rawTimestamp);
|
||||||
} else {
|
} else {
|
||||||
// Legacy fallback: field "waktu" format "HH:mm:ss"
|
|
||||||
final waktuStr = json['waktu'] as String?;
|
final waktuStr = json['waktu'] as String?;
|
||||||
if (waktuStr != null) {
|
if (waktuStr != null) {
|
||||||
final parts = waktuStr.split(':');
|
final parts = waktuStr.split(':');
|
||||||
|
|
@ -175,6 +287,20 @@ class Evaporasi {
|
||||||
acuanPagi: acuanPagiVal,
|
acuanPagi: acuanPagiVal,
|
||||||
status: statusVal,
|
status: statusVal,
|
||||||
timestamp: timestamp,
|
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