percobaan

This commit is contained in:
Mochamad ongki ramadani 2026-05-09 18:00:57 +07:00
parent fff7f8b5db
commit f9abec81c3
6 changed files with 357 additions and 141 deletions

View File

@ -27,29 +27,24 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
on<EvaporasiViewModeChanged>(_onViewModeChanged);
}
/// =========================
/// 🚀 START
/// =========================
Future<void> _onStarted(
WatchEvaporasiStarted event,
Emitter<EvaporasiState> emit,
) async {
// chartLabels & listData untuk list/label di UI
emit(state.copyWith(isLoading: true));
// Ambil history yang akan dipakai untuk list + agregasi grafik.
final history = await _repository.getSensorHistory(
'evaporasi/history',
'Monitoring/History',
(json) => Evaporasi.fromJson(json),
orderByChild: null,
limit: 500,
);
// listData dipakai untuk tab/list di UI (default: ambil data yang sudah ada di Firebase)
final listData = List<Evaporasi>.from(history)
..sort((a, b) => a.timestamp.compareTo(b.timestamp));
final dailyGraph = TimeSeriesMapper.toDaily(
data: history,
getTime: (e) => e.timestamp,
getValue: (e) => e.evaporasi,
@ -61,7 +56,30 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
getValue: (e) => e.suhu,
);
// Hitung status dari data terakhir history jika ada
final weeklyGraph = TimeSeriesMapper.toWeekly(
data: history,
getTime: (e) => e.timestamp,
getValue: (e) => e.evaporasi,
);
final monthlyGraph = TimeSeriesMapper.toMonthly(
data: history,
getTime: (e) => e.timestamp,
getValue: (e) => e.evaporasi,
);
final weeklyTemp = TimeSeriesMapper.toWeekly(
data: history,
getTime: (e) => e.timestamp,
getValue: (e) => e.suhu,
);
final monthlyTemp = TimeSeriesMapper.toMonthly(
data: history,
getTime: (e) => e.timestamp,
getValue: (e) => e.suhu,
);
final lastValue = history.isNotEmpty ? history.last.evaporasi : 0.0;
final (status, rain) = _computeWeatherStatus(lastValue);
_emitEvaporasiAlert(status, rain, lastValue);
@ -71,9 +89,11 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
listData: listData,
dailyValues: dailyGraph,
dailyTemperatures: dailyTempGraph,
chartLabels: _buildChartLabels(
period: 'Hari Ini',
),
weeklyValues: weeklyGraph,
monthlyValues: monthlyGraph,
weeklyTemperatures: weeklyTemp,
monthlyTemperatures: monthlyTemp,
chartLabels: _buildChartLabels(period: 'Hari Ini'),
weatherStatus: status,
willRain: rain,
isLoading: false,
@ -85,18 +105,16 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
.listen((data) => add(_EvaporasiRealtimeUpdated(data)));
}
/// =========================
/// REALTIME
/// =========================
void _onRealtimeUpdated(
_EvaporasiRealtimeUpdated event,
Emitter<EvaporasiState> emit,
) {
// Hanya update grafik daily (index jam saat ini).
final updated = List<double>.from(state.dailyValues);
final updatedTemp = List<double>.from(state.dailyTemperatures);
final index = DateTime.now().hour;
if (index < updated.length) {
if (index >= 0 && index < updated.length) {
updated[index] = event.data.evaporasi;
updatedTemp[index] = event.data.suhu;
}
@ -115,20 +133,18 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
));
}
/// =========================
/// 📊 PERIOD
/// =========================
Future<void> _onPeriodChanged(
EvaporasiPeriodChanged event,
Emitter<EvaporasiState> emit,
) async {
emit(state.copyWith(isLoading: true, selectedPeriod: event.period));
final history = state.history;
List<double> updated;
List<double> updatedTemp;
if (event.period == "Minggu Ini") {
if (event.period == 'Minggu Ini') {
updated = TimeSeriesMapper.toWeekly(
data: history,
getTime: (e) => e.timestamp,
@ -139,7 +155,7 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
getTime: (e) => e.timestamp,
getValue: (e) => e.suhu,
);
} else if (event.period == "Bulan Ini") {
} else if (event.period == 'Bulan Ini') {
updated = TimeSeriesMapper.toMonthly(
data: history,
getTime: (e) => e.timestamp,
@ -163,18 +179,17 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
);
}
// Pertahankan status cuaca saat ini (tidak berubah karena hanya ganti periode)
emit(state.copyWith(
dailyValues: updated,
dailyTemperatures: updatedTemp,
chartLabels: _buildChartLabels(period: event.period),
isLoading: false,
// penting: jangan hilangkan list
listData: state.listData,
history: state.history,
));
}
/// =========================
/// 📅 DATE SELECTED (Custom Date)
/// =========================
Future<void> _onDateSelected(
EvaporasiDateSelected event,
Emitter<EvaporasiState> emit,
@ -206,18 +221,17 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
dailyTemperatures: updatedTemp,
chartLabels: _buildChartLabels(period: 'Tanggal Khusus'),
isLoading: false,
// jangan hilangkan list
listData: state.listData,
history: state.history,
));
}
/// =========================
/// 🔄 VIEW MODE CHANGED
/// =========================
Future<void> _onViewModeChanged(
EvaporasiViewModeChanged event,
Emitter<EvaporasiState> emit,
) async {
if (event.mode == EvaporasiViewMode.period) {
// Kembali ke mode period - trigger period change
add(EvaporasiPeriodChanged(state.selectedPeriod));
} else {
emit(state.copyWith(viewMode: event.mode));
@ -230,31 +244,12 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
return super.close();
}
/// =========================
/// 🌤 HELPER: Status Cuaca
/// =========================
static (String status, bool willRain) _computeWeatherStatus(double value) {
if (value <= 5.0) return ('Baik', false);
if (value <= 10.0) return ('Sedang', true);
return ('Buruk', true);
}
List<String> _buildChartLabels({required String period}) {
if (period == 'Minggu Ini') {
return const ['Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab', 'Min'];
}
// Untuk Bulan Ini & Tanggal Khusus/ Hari Ini, biarkan chart pakai 0..23 / 0..days
final now = DateTime.now();
if (period == 'Bulan Ini') {
final daysInMonth = DateTime(now.year, now.month + 1, 0).day;
return List.generate(daysInMonth, (i) => '${i + 1}');
}
// Hari Ini / Tanggal Khusus => 24 jam
return List.generate(24, (i) => '${i.toString().padLeft(2, '0')}:00');
}
void _emitEvaporasiAlert(String status, bool willRain, double value) {
final AlertSeverity severity;
final String message;
@ -268,7 +263,7 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
message =
'Evaporasi ${value.toStringAsFixed(1)} mm — status sedang, potensi hujan';
} else {
severity = AlertSeverity.info; // normal bersihkan alert
severity = AlertSeverity.info;
message = '';
}
@ -282,9 +277,23 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
),
));
}
List<String> _buildChartLabels({required String period}) {
if (period == 'Minggu Ini') {
return const ['Sen', 'Sel', 'Rab', 'Kam', 'Jum', 'Sab', 'Min'];
}
final now = DateTime.now();
if (period == 'Bulan Ini') {
final daysInMonth = DateTime(now.year, now.month + 1, 0).day;
return List.generate(daysInMonth, (i) => '${i + 1}');
}
// Hari Ini / Tanggal Khusus => 24 jam
return List.generate(24, (i) => '${i.toString().padLeft(2, '0')}:00');
}
}
/// INTERNAL EVENT
class _EvaporasiRealtimeUpdated extends EvaporasiEvent {
final Evaporasi data;
@ -293,3 +302,4 @@ class _EvaporasiRealtimeUpdated extends EvaporasiEvent {
@override
List<Object> get props => [data];
}

View File

@ -10,8 +10,13 @@ class EvaporasiState extends Equatable {
final DateTime? selectedDate; // tanggal spesifik untuk custom date
final EvaporasiViewMode viewMode; // period vs custom date
final List<double> dailyValues; // untuk grafik evaporasi
final List<double> dailyValues; // untuk grafik evaporasi (default)
final List<double> weeklyValues;
final List<double> monthlyValues;
final List<double> dailyTemperatures; // untuk grafik suhu
final List<double> weeklyTemperatures;
final List<double> monthlyTemperatures;
/// Label X-axis sesuai agregasi period
/// Contoh: Hari Ini => ["00:00","01:00",...]
@ -21,6 +26,7 @@ class EvaporasiState extends Equatable {
/// Data untuk list (timestamp asli dari firebase)
final List<Evaporasi> listData;
final List<Evaporasi> history;
final String weatherStatus; // Baik / Sedang / Buruk
@ -32,15 +38,19 @@ class EvaporasiState extends Equatable {
this.currentValue = 0.0,
this.temperature = 0.0,
this.waterLevel = 0.0,
this.selectedPeriod = "Hari Ini",
this.selectedPeriod = 'Hari Ini',
this.selectedDate,
this.viewMode = EvaporasiViewMode.period,
this.dailyValues = const [],
this.weeklyValues = const [],
this.monthlyValues = const [],
this.dailyTemperatures = const [],
this.weeklyTemperatures = const [],
this.monthlyTemperatures = const [],
this.chartLabels = const [],
this.listData = const [],
this.history = const [],
this.weatherStatus = "Baik",
this.weatherStatus = 'Baik',
this.willRain = false,
this.isLoading = true,
});
@ -53,7 +63,11 @@ EvaporasiState copyWith({
DateTime? selectedDate,
EvaporasiViewMode? viewMode,
List<double>? dailyValues,
List<double>? weeklyValues,
List<double>? monthlyValues,
List<double>? dailyTemperatures,
List<double>? weeklyTemperatures,
List<double>? monthlyTemperatures,
List<String>? chartLabels,
List<Evaporasi>? listData,
List<Evaporasi>? history,
@ -69,10 +83,14 @@ EvaporasiState copyWith({
selectedDate: selectedDate ?? this.selectedDate,
viewMode: viewMode ?? this.viewMode,
dailyValues: dailyValues ?? this.dailyValues,
weeklyValues: weeklyValues ?? this.weeklyValues,
monthlyValues: monthlyValues ?? this.monthlyValues,
dailyTemperatures: dailyTemperatures ?? this.dailyTemperatures,
history: history ?? this.history,
weeklyTemperatures: weeklyTemperatures ?? this.weeklyTemperatures,
monthlyTemperatures: monthlyTemperatures ?? this.monthlyTemperatures,
chartLabels: chartLabels ?? this.chartLabels,
listData: listData ?? this.listData,
history: history ?? this.history,
weatherStatus: weatherStatus ?? this.weatherStatus,
willRain: willRain ?? this.willRain,
isLoading: isLoading ?? this.isLoading,
@ -88,10 +106,17 @@ EvaporasiState copyWith({
selectedDate,
viewMode,
dailyValues,
weeklyValues,
monthlyValues,
dailyTemperatures,
weeklyTemperatures,
monthlyTemperatures,
chartLabels,
listData,
history,
weatherStatus,
willRain,
isLoading,
];
}

View File

@ -1,15 +1,21 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import '../blocs/evaporasi_bloc.dart';
import 'widgets/evaporasi_chart_widget.dart';
import 'widgets/evaporasi_period_selector.dart';
import '../../shared/utils/pdf/pdf_export_service.dart';
import '../../shared/widgets/export_pdf_button.dart';
class EvaporasiScreen extends StatelessWidget {
class EvaporasiScreen extends StatefulWidget {
const EvaporasiScreen({super.key});
@override
State<EvaporasiScreen> createState() => _EvaporasiScreenState();
}
class _EvaporasiScreenState extends State<EvaporasiScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
@ -49,9 +55,10 @@ class EvaporasiScreen extends StatelessWidget {
const SizedBox(height: 25),
_statusCard(state),
const SizedBox(height: 25),
const Text("Tren Evaporasi & Suhu",
style:
TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const Text(
"Tren Evaporasi & Suhu",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 15),
// Period selector with date picker
@ -61,7 +68,6 @@ class EvaporasiScreen extends StatelessWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Show selected date info when in custom mode
if (state.viewMode == EvaporasiViewMode.customDate &&
state.selectedDate != null)
Padding(
@ -80,28 +86,32 @@ class EvaporasiScreen extends StatelessWidget {
);
},
),
const SizedBox(height: 15),
// Chart
Builder(
builder: (context) {
final state = context.watch<EvaporasiBloc>().state;
return Column(
children: [
EvaporasiChartWidget(
return EvaporasiChartWidget(
dailyValues: state.dailyValues,
dailyTemperatures: state.dailyTemperatures,
period: state.viewMode == EvaporasiViewMode.customDate
? "Tanggal Khusus"
: state.selectedPeriod,
chartLabels: state.chartLabels,
),
const SizedBox(height: 20),
_evaporasiList(state),
const SizedBox(height: 10),
],
);
},
),
const SizedBox(height: 20),
// List data (mengikuti chart: period vs custom date)
_evaporasiList(state),
const SizedBox(height: 10),
ExportPdfButton(
onExport: () => PdfExportService.evaporasi(
evaporasi: state.currentValue,
@ -224,6 +234,20 @@ class EvaporasiScreen extends StatelessWidget {
break;
}
// Teks peringatan khusus evaporasi (normal/sedang/tinggi)
final String warningText;
if (state.weatherStatus == 'Baik') {
warningText =
'Normal — evaporasi stabil, risiko dampak rendah.';
} else if (state.weatherStatus == 'Sedang') {
warningText =
'Sedang — evaporasi mulai tinggi, pantau kondisi cuaca.';
} else {
warningText =
'Tinggi — evaporasi signifikan, berpotensi memengaruhi kondisi lingkungan.';
}
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
@ -246,22 +270,28 @@ class EvaporasiScreen extends StatelessWidget {
),
const SizedBox(height: 4),
Text(
state.weatherStatus,
state.weatherStatus == 'Baik'
? 'Normal'
: state.weatherStatus == 'Sedang'
? 'Sedang'
: 'Tinggi',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: statusColor,
),
),
if (state.willRain)
const Text(
"⚠️ Potensi hujan tinggi",
style: TextStyle(
Text(
warningText,
style: const TextStyle(
fontSize: 12,
color: Colors.red,
fontWeight: FontWeight.w500,
),
),
],
),
),
@ -274,7 +304,15 @@ class EvaporasiScreen extends StatelessWidget {
/// 🧾 LIST DATA EVAPORASI
/// =========================
Widget _evaporasiList(EvaporasiState state) {
final data = state.listData;
final data = state.viewMode == EvaporasiViewMode.customDate && state.selectedDate != null
? state.history
.where((e) =>
e.timestamp.year == state.selectedDate!.year &&
e.timestamp.month == state.selectedDate!.month &&
e.timestamp.day == state.selectedDate!.day)
.toList()
: state.history;
if (data.isEmpty) {
return Container(
width: double.infinity,
@ -283,8 +321,10 @@ class EvaporasiScreen extends StatelessWidget {
color: Colors.white,
borderRadius: BorderRadius.circular(20),
),
child: const Text('Belum ada data evaporasi',
style: TextStyle(color: Colors.grey)),
child: const Text(
'Belum ada data evaporasi',
style: TextStyle(color: Colors.grey),
),
);
}
@ -303,17 +343,14 @@ class EvaporasiScreen extends StatelessWidget {
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
ListView.separated(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
SizedBox(
height: 280,
child: ListView.separated(
itemCount: data.length,
separatorBuilder: (_, __) => const Divider(height: 1),
itemBuilder: (context, index) {
final e = data[index];
final dateLabel = DateFormat('dd MMM yyyy', 'id_ID')
.format(e.timestamp) +
'' +
DateFormat('HH:mm:ss', 'id_ID').format(e.timestamp);
final dateLabel =
'${DateFormat('dd MMM yyyy', 'id_ID').format(e.timestamp)}${DateFormat('HH:mm:ss', 'id_ID').format(e.timestamp)}';
return Padding(
padding: const EdgeInsets.symmetric(vertical: 10),
@ -344,19 +381,39 @@ class EvaporasiScreen extends StatelessWidget {
),
const SizedBox(height: 4),
Text(
'${e.suhu.toStringAsFixed(1)} °C',
'Tinggi Air: ${e.tinggiAir.toStringAsFixed(1)} cm',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.blue.shade700,
),
),
const SizedBox(height: 4),
Text(
'Suhu: ${e.suhu.toStringAsFixed(1)} °C',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.orange.shade700,
),
),
const SizedBox(height: 4),
Text(
_statusTextForHistory(e.evaporasi),
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w700,
color: _statusColorForHistory(e.evaporasi),
),
),
],
),
],
),
);
},
separatorBuilder: (_, __) => const Divider(height: 1),
),
),
],
),
@ -366,8 +423,22 @@ class EvaporasiScreen extends StatelessWidget {
/// =========================
/// 📅 FORMAT DATE INFO (for custom date display)
/// =========================
String _statusTextForHistory(double evaporasi) {
if (evaporasi <= 5.0) return 'Status: Normal';
if (evaporasi <= 10.0) return 'Status: Sedang';
return 'Status: Tinggi';
}
Color _statusColorForHistory(double evaporasi) {
if (evaporasi <= 5.0) return Colors.green;
if (evaporasi <= 10.0) return Colors.orange;
return Colors.red;
}
String _formatDateInfo(DateTime date) {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final yesterday = today.subtract(const Duration(days: 1));
final selected = DateTime(date.year, date.month, date.day);
@ -381,3 +452,4 @@ class EvaporasiScreen extends StatelessWidget {
}
}
}

View File

@ -72,18 +72,42 @@ class EvaporasiChartWidget extends StatelessWidget {
duration: const Duration(milliseconds: 600),
curve: Curves.easeOutCubic,
LineChartData(
minY: 0,
maxY: maxY,
gridData: const FlGridData(show: false),
borderData: FlBorderData(show: false),
titlesData: FlTitlesData(
show: true,
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false)),
// dual axis: kiri untuk Evaporasi (mm), kanan untuk Suhu (°C)
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 40,
interval: _getYAxisInterval(safeValues),
getTitlesWidget: (value, meta) {
return Text(
value.toStringAsFixed(0),
style: const TextStyle(color: Colors.blueGrey, fontSize: 10),
);
},
),
),
rightTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 40,
interval: _getYAxisInterval(safeTemps),
getTitlesWidget: (value, meta) {
return Text(
value.toStringAsFixed(0),
style: const TextStyle(color: Colors.orangeAccent, fontSize: 10),
);
},
),
),
topTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false)),
leftTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false)),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
@ -105,7 +129,7 @@ class EvaporasiChartWidget extends StatelessWidget {
),
),
lineBarsData: [
// === Garis Evaporasi ===
// === Garis Evaporasi (kiri axis) ===
LineChartBarData(
spots: safeValues.asMap().entries.map((e) {
return FlSpot(e.key.toDouble(), e.value);
@ -143,7 +167,7 @@ class EvaporasiChartWidget extends StatelessWidget {
),
),
),
// === Garis Suhu ===
// === Garis Suhu (kanan axis) ===
LineChartBarData(
spots: safeTemps.asMap().entries.map((e) {
return FlSpot(e.key.toDouble(), e.value);
@ -245,5 +269,15 @@ class EvaporasiChartWidget extends StatelessWidget {
if (period == "Minggu Ini") return 1;
return 1;
}
double _getYAxisInterval(List<double> data) {
if (data.isEmpty) return 1;
final max = data.reduce((a, b) => a > b ? a : b);
if (max <= 10) return 2;
if (max <= 20) return 5;
if (max <= 30) return 10;
return 20;
}
}

View File

@ -0,0 +1,72 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:monitoring_repository/monitoring_repository.dart';
class EvaporasiDateSearchBar extends StatefulWidget {
final String initialQuery;
final ValueChanged<String> onQueryChanged;
const EvaporasiDateSearchBar({
super.key,
required this.initialQuery,
required this.onQueryChanged,
});
@override
State<EvaporasiDateSearchBar> createState() => _EvaporasiDateSearchBarState();
}
class _EvaporasiDateSearchBarState extends State<EvaporasiDateSearchBar> {
late final TextEditingController _controller;
DateTime? _selected;
@override
void initState() {
super.initState();
_controller = TextEditingController(text: widget.initialQuery);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
bool _matches(Evaporasi item, String query) {
if (query.trim().isEmpty) return true;
final date = item.timestamp;
final ddMMyyyy = DateFormat('dd/MM/yyyy', 'id_ID').format(date);
final ddMMMYYYY = DateFormat('dd MMM yyyy', 'id_ID').format(date);
final q = query.trim().toLowerCase();
return ddMMyyyy.toLowerCase().contains(q) || ddMMMYYYY.toLowerCase().contains(q);
}
@override
Widget build(BuildContext context) {
return TextField(
controller: _controller,
decoration: InputDecoration(
hintText: 'Cari tanggal (dd/MM/yyyy)',
prefixIcon: const Icon(Icons.search),
suffixIcon: _controller.text.isNotEmpty
? IconButton(
icon: const Icon(Icons.clear),
onPressed: () => setState(() => _controller.clear()),
)
: null,
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
isDense: true,
),
onChanged: (v) {
widget.onQueryChanged(v);
setState(() {});
},
);
}
}

View File

@ -32,9 +32,12 @@ class Evaporasi implements HasTimestamp {
final suhu = (json['suhu'] ?? json['suhu_air'] ?? 0).toDouble();
final tinggi = (json['tinggi'] ?? json['tinggi_air'] ?? 0).toDouble();
// Prioritas: timestamp (Unix, kalau firmware sudah diupdate)
// fallback ke waktu ("HH:MM:SS")
final rawTime = json['timestamp'] ?? json['waktu'];
// Prioritas waktu sesuai firmware ESP32:
// 1) timestamp (Unix, kalau pernah dikirim)
// 2) datetime ("YYYY-MM-DD HH:MM:SS")
// 3) waktu ("HH:MM:SS") fallback (akan memakai tanggal hari ini)
final rawTime = json['timestamp'] ?? json['datetime'] ?? json['waktu'];
return Evaporasi(
evaporasi: (json['evaporasi'] ?? 0).toDouble(),