update pembaruan time series mapper

This commit is contained in:
kleponijo 2026-05-06 17:47:40 +07:00
parent e37fd688fb
commit 41e707d209
13 changed files with 206 additions and 214 deletions

View File

@ -1,3 +1,5 @@
import 'package:monitoring_repository/monitoring_repository.dart';
class TimeSeriesMapper {
/// =========================
/// 📅 DAILY (24 JAM)
@ -114,4 +116,30 @@ class TimeSeriesMapper {
return result;
}
// Tambahkan ini setelah method smooth()
/// Shortcut untuk model yang implement HasTimestamp.
/// Dari: TimeSeriesMapper.toDaily(data: h, getTime: (e) => e.timestamp, getValue: (e) => e.speed)
/// Ke: TimeSeriesMapper.dailyFrom(h, (e) => e.speed)
static List<double> dailyFrom<T extends HasTimestamp>(
List<T> data,
double Function(T) getValue,
) =>
smooth(
toDaily(data: data, getTime: (e) => e.timestamp, getValue: getValue));
static List<double> weeklyFrom<T extends HasTimestamp>(
List<T> data,
double Function(T) getValue,
) =>
smooth(toWeekly(
data: data, getTime: (e) => e.timestamp, getValue: getValue));
static List<double> monthlyFrom<T extends HasTimestamp>(
List<T> data,
double Function(T) getValue,
) =>
smooth(toMonthly(
data: data, getTime: (e) => e.timestamp, getValue: getValue));
}

View File

@ -1,6 +0,0 @@
// Perlu handle semua kasus ini:
TimestampParser.parse(1777807041) // Unix detik DateTime
TimestampParser.parse(1777807041000) // Unix milidetik DateTime
TimestampParser.parse("14:26:01") // String jam saja DateTime hari ini
TimestampParser.parse("2025-03-05T14:26:01") // ISO string DateTime
TimestampParser.parse(null) // fallback DateTime.now()

View File

@ -35,26 +35,29 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
emit(state.copyWith(isLoading: true));
final history = await _repository.getSensorHistory(
'evaporasi/history',
'Monitoring/History',
(json) => Evaporasi.fromJson(json),
orderByChild: null, // waktu = string, tidak bisa orderByChild
limit: 500,
);
final dailyGraph = TimeSeriesMapper.toDaily(
data: history,
getTime: (e) => e.timestamp,
getValue: (e) => e.evaporasi,
);
final dailyTempGraph = TimeSeriesMapper.toDaily(
data: history,
getTime: (e) => e.timestamp,
getValue: (e) => e.suhu,
);
final dailyGraph =
TimeSeriesMapper.dailyFrom<Evaporasi>(history, (e) => e.evaporasi);
final dailyTempGraph =
TimeSeriesMapper.dailyFrom<Evaporasi>(history, (e) => e.suhu);
final weeklyGraph =
TimeSeriesMapper.weeklyFrom<Evaporasi>(history, (e) => e.evaporasi);
final monthlyGraph =
TimeSeriesMapper.monthlyFrom<Evaporasi>(history, (e) => e.evaporasi);
final weeklyTemp =
TimeSeriesMapper.weeklyFrom<Evaporasi>(history, (e) => e.suhu);
final monthlyTemp =
TimeSeriesMapper.monthlyFrom<Evaporasi>(history, (e) => e.suhu);
// Hitung status dari data terakhir history jika ada
final lastValue = history.isNotEmpty ? history.last.evaporasi : 0.0;
final (status, rain) = _computeWeatherStatus(lastValue);
_emitEvaporasiAlert(status, rain, lastValue);
_emitEvaporasiAlert(status, lastValue);
emit(state.copyWith(
history: history,
@ -63,6 +66,10 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
weatherStatus: status,
willRain: rain,
isLoading: false,
weeklyValues: weeklyGraph,
monthlyValues: monthlyGraph,
weeklyTemperatures: weeklyTemp,
monthlyTemperatures: monthlyTemp,
));
await _subscription?.cancel();
@ -88,7 +95,7 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
}
final (status, rain) = _computeWeatherStatus(event.data.evaporasi);
_emitEvaporasiAlert(status, rain, event.data.evaporasi);
_emitEvaporasiAlert(status, event.data.evaporasi);
emit(state.copyWith(
currentValue: event.data.evaporasi,
@ -104,57 +111,11 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
/// =========================
/// 📊 PERIOD
/// =========================
Future<void> _onPeriodChanged(
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") {
updated = TimeSeriesMapper.toWeekly(
data: history,
getTime: (e) => e.timestamp,
getValue: (e) => e.evaporasi,
);
updatedTemp = TimeSeriesMapper.toWeekly(
data: history,
getTime: (e) => e.timestamp,
getValue: (e) => e.suhu,
);
} else if (event.period == "Bulan Ini") {
updated = TimeSeriesMapper.toMonthly(
data: history,
getTime: (e) => e.timestamp,
getValue: (e) => e.evaporasi,
);
updatedTemp = TimeSeriesMapper.toMonthly(
data: history,
getTime: (e) => e.timestamp,
getValue: (e) => e.suhu,
);
} else {
updated = TimeSeriesMapper.toDaily(
data: history,
getTime: (e) => e.timestamp,
getValue: (e) => e.evaporasi,
);
updatedTemp = TimeSeriesMapper.toDaily(
data: history,
getTime: (e) => e.timestamp,
getValue: (e) => e.suhu,
);
}
// Pertahankan status cuaca saat ini (tidak berubah karena hanya ganti periode)
emit(state.copyWith(
dailyValues: updated,
dailyTemperatures: updatedTemp,
isLoading: false,
));
) {
emit(state.copyWith(selectedPeriod: event.period));
}
@override
@ -172,7 +133,7 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
return ('Buruk', true);
}
void _emitEvaporasiAlert(String status, bool willRain, double value) {
void _emitEvaporasiAlert(String status, double value) {
final AlertSeverity severity;
final String message;

View File

@ -7,12 +7,17 @@ class EvaporasiState extends Equatable {
final String selectedPeriod;
final List<double> dailyValues; // untuk grafik evaporasi
final List<double> weeklyValues;
final List<double> monthlyValues;
final List<double> dailyTemperatures; // untuk grafik suhu
final List<double> weeklyTemperatures;
final List<double> monthlyTemperatures;
final List<Evaporasi> history;
final String weatherStatus; // Baik / Sedang / Buruk
final bool willRain; // true jika status Sedang/Buruk
final String weatherStatus;
final bool willRain;
final bool isLoading;
const EvaporasiState({
@ -21,7 +26,11 @@ class EvaporasiState extends Equatable {
this.waterLevel = 0.0,
this.selectedPeriod = "Hari Ini",
this.dailyValues = const [],
this.weeklyValues = const [],
this.monthlyValues = const [],
this.dailyTemperatures = const [],
this.weeklyTemperatures = const [],
this.monthlyTemperatures = const [],
this.history = const [],
this.weatherStatus = "Baik",
this.willRain = false,
@ -34,7 +43,11 @@ class EvaporasiState extends Equatable {
double? waterLevel,
String? selectedPeriod,
List<double>? dailyValues,
List<double>? weeklyValues,
List<double>? monthlyValues,
List<double>? dailyTemperatures,
List<double>? weeklyTemperatures,
List<double>? monthlyTemperatures,
List<Evaporasi>? history,
String? weatherStatus,
bool? willRain,
@ -46,7 +59,11 @@ class EvaporasiState extends Equatable {
waterLevel: waterLevel ?? this.waterLevel,
selectedPeriod: selectedPeriod ?? this.selectedPeriod,
dailyValues: dailyValues ?? this.dailyValues,
weeklyValues: weeklyValues ?? this.weeklyValues,
monthlyValues: monthlyValues ?? this.monthlyValues,
dailyTemperatures: dailyTemperatures ?? this.dailyTemperatures,
weeklyTemperatures: weeklyTemperatures ?? this.weeklyTemperatures,
monthlyTemperatures: monthlyTemperatures ?? this.monthlyTemperatures,
history: history ?? this.history,
weatherStatus: weatherStatus ?? this.weatherStatus,
willRain: willRain ?? this.willRain,
@ -61,7 +78,11 @@ class EvaporasiState extends Equatable {
waterLevel,
selectedPeriod,
dailyValues,
weeklyValues,
monthlyValues,
dailyTemperatures,
weeklyTemperatures,
monthlyTemperatures,
history,
weatherStatus,
willRain,

View File

@ -48,32 +48,16 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
final history = await _repository.getSensorHistory(
'anemometer/history',
(json) => MyWindSpeed.fromJson(json),
orderByChild: 'timestamp',
limit: 500,
);
// Ini saja yang benar
final dailyGraph = TimeSeriesMapper.smooth(
TimeSeriesMapper.toDaily(
data: history,
getTime: (e) => e.timestamp,
getValue: (e) => e.speed,
),
);
final weekly = TimeSeriesMapper.smooth(
TimeSeriesMapper.toWeekly(
data: history,
getTime: (e) => e.timestamp,
getValue: (e) => e.speed,
),
);
final monthly = TimeSeriesMapper.smooth(
TimeSeriesMapper.toMonthly(
data: history,
getTime: (e) => e.timestamp,
getValue: (e) => e.speed,
),
);
final dailyGraph =
TimeSeriesMapper.dailyFrom<MyWindSpeed>(history, (e) => e.speed);
final weekly =
TimeSeriesMapper.weeklyFrom<MyWindSpeed>(history, (e) => e.speed);
final monthly =
TimeSeriesMapper.monthlyFrom<MyWindSpeed>(history, (e) => e.speed);
// Cek kondisi awal dari history
if (history.isNotEmpty) {
@ -141,48 +125,11 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
/// =========================
/// 📊 CHANGE PERIOD
/// =========================
Future<void> _onPeriodChanged(
void _onPeriodChanged(
WindSpeedPeriodChanged event,
Emitter<WindSpeedState> emit,
) async {
) {
emit(state.copyWith(selectedPeriod: event.period));
final history = state.history;
List<double> raw;
if (event.period == "Minggu Ini") {
raw = TimeSeriesMapper.toWeekly(
data: history,
getTime: (e) => e.timestamp,
getValue: (e) => e.speed,
);
} else if (event.period == "Bulan Ini") {
raw = TimeSeriesMapper.toMonthly(
data: history,
getTime: (e) => e.timestamp,
getValue: (e) => e.speed,
);
} else {
raw = TimeSeriesMapper.toDaily(
data: history,
getTime: (e) => e.timestamp,
getValue: (e) => e.speed,
);
}
/// 🔥 baru di-smooth
final updatedGraph = TimeSeriesMapper.smooth(raw);
emit(state.copyWith(
dailySpeeds:
event.period == "Hari Ini" ? updatedGraph : state.dailySpeeds,
weeklySpeeds:
event.period == "Minggu Ini" ? updatedGraph : state.weeklySpeeds,
monthlySpeeds:
event.period == "Bulan Ini" ? updatedGraph : state.monthlySpeeds,
isLoading: false,
));
}
@override

View File

@ -3,3 +3,4 @@ library monitoring_repository;
export 'src/monitoring_repo.dart';
export 'src/firebase_monitoring_repo.dart';
export 'src/models/models.dart';
export 'src/utils/timestamp_parser.dart';

View File

@ -1,72 +1,61 @@
// import 'dart:developer';
import 'dart:async';
// import 'package:rxdart/rxdart.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:monitoring_repository/monitoring_repository.dart';
import 'monitoring_repo.dart';
import 'models/has_timestamp.dart';
import 'models/models.dart';
/// === ambil data dari realtime database firebase === ///
class FirebaseMonitoringRepo implements MonitoringRepository {
final FirebaseDatabase _db = FirebaseDatabase.instance;
// Satu fungsi untuk semua jenis sensor
// Kamu cukup masukkan "path" database-nya saja
@override
Stream<T> getSensorStream<T>(
String path,
T Function(Map<dynamic, dynamic> json) mapper,
) {
/// === ambil data mentah dari firebase === ///
return _db.ref(path).onValue.map((event) {
final Object? value = event.snapshot.value;
if (value is Map) {
return mapper(value);
} else {
// Jika data kosong atau bukan Map, berikan Map kosong agar mapper tidak crash
return mapper({});
}
final value = event.snapshot.value;
return mapper(value is Map ? value : {});
});
}
@override
// Jika ingin mengambil data sekali saja (bukan stream)
Future<T> getSensorSnapshot<T>(
String path,
T Function(Map<dynamic, dynamic> json) mapper,
) async {
final snapshot = await _db.ref(path).get();
final data = snapshot.value as Map<dynamic, dynamic>? ?? {};
return mapper(data);
return mapper(snapshot.value as Map<dynamic, dynamic>? ?? {});
}
@override
Future<List<T>> getSensorHistory<T>(
Future<List<T>> getSensorHistory<T extends HasTimestamp>(
String path,
T Function(Map<dynamic, dynamic> json) mapper,
) async {
T Function(Map<dynamic, dynamic> json) mapper, {
String? orderByChild,
int limit = 500,
}) async {
try {
// Ambil data dari path history
final snapshot = await _db.ref(path).get();
Query query = _db.ref(path);
if (snapshot.exists && snapshot.value is Map) {
final Map<dynamic, dynamic> data = snapshot.value as Map;
final list = data.values.map((item) {
return mapper(item as Map<dynamic, dynamic>);
}).toList();
// Sort by timestamp Firebase push tidak jamin urutan
list.sort((a, b) {
if (a is MyWindSpeed && b is MyWindSpeed) {
return a.timestamp.compareTo(b.timestamp);
if (orderByChild != null) {
query = query.orderByChild(orderByChild).limitToLast(limit);
} else {
query = query.limitToLast(limit);
}
return 0;
});
final snapshot = await query.get();
if (!snapshot.exists || snapshot.value is! Map) return [];
final data = snapshot.value as Map<dynamic, dynamic>;
final list = data.values
.whereType<Map>()
.map((item) => mapper(item))
.toList();
// Sort generik works untuk SEMUA sensor karena T extends HasTimestamp
list.sort((a, b) => a.timestamp.compareTo(b.timestamp));
return list;
}
return [];
} catch (e) {
} catch (_) {
return [];
}
}

View File

@ -1,7 +1,13 @@
class Evaporasi {
import 'has_timestamp.dart';
import '../utils/timestamp_parser.dart';
class Evaporasi implements HasTimestamp {
final double evaporasi;
final double suhu;
final double tinggiAir;
final String status;
@override
final DateTime timestamp;
Evaporasi({
@ -9,6 +15,7 @@ class Evaporasi {
required this.suhu,
required this.tinggiAir,
required this.timestamp,
this.status = '',
});
static final empty = Evaporasi(
@ -19,27 +26,22 @@ class Evaporasi {
);
factory Evaporasi.fromJson(Map<dynamic, dynamic> json) {
final now = DateTime.now();
DateTime timestamp = now;
// Handle perbedaan field name antara history dan realtime:
// History path : suhu, tinggi, evaporasi, waktu
// Realtime path : suhu_air, tinggi_air, evaporasi, waktu, status
final suhu = (json['suhu'] ?? json['suhu_air'] ?? 0).toDouble();
final tinggi = (json['tinggi'] ?? json['tinggi_air'] ?? 0).toDouble();
final waktuStr = json['waktu'] as String?;
if (waktuStr != null) {
final parts = waktuStr.split(':');
if (parts.length >= 2) {
final jam = int.tryParse(parts[0]) ?? 0;
final menit = int.tryParse(parts[1]) ?? 0;
final detik = parts.length >= 3 ? (int.tryParse(parts[2]) ?? 0) : 0;
timestamp = DateTime(now.year, now.month, now.day, jam, menit, detik);
}
}
// Prioritas: timestamp (Unix, kalau firmware sudah diupdate)
// fallback ke waktu ("HH:MM:SS")
final rawTime = json['timestamp'] ?? json['waktu'];
return Evaporasi(
evaporasi: (json['evaporasi'] ?? 0).toDouble(),
suhu: (json['suhu_air'] ?? 0).toDouble(),
tinggiAir: (json['tinggi_air'] ?? 0).toDouble(),
/// 🔥 bikin timestamp dari jam & menit
timestamp: timestamp,
suhu: suhu,
tinggiAir: tinggi,
status: json['status'] ?? '',
timestamp: TimestampParser.parse(rawTime), // pakai TimestampParser
);
}
}

View File

@ -0,0 +1,5 @@
/// Semua model sensor wajib implement ini.
/// Memungkinkan sorting & TimeSeriesMapper yang generik.
abstract class HasTimestamp {
DateTime get timestamp;
}

View File

@ -1,7 +1,4 @@
export 'wind_speed.dart';
export 'evaporasi.dart';
export 'atmospheric_conditions.dart';
abstract class HasTimestamp {
DateTime get timestamp;
}
export 'has_timestamp.dart';

View File

@ -1,23 +1,23 @@
class MyWindSpeed {
double speed;
import 'has_timestamp.dart';
import '../utils/timestamp_parser.dart';
DateTime timestamp;
class MyWindSpeed implements HasTimestamp {
final double speed;
@override
final DateTime timestamp;
MyWindSpeed({required this.speed, required this.timestamp});
static final empty = MyWindSpeed(
speed: 0.0,
timestamp: DateTime.fromMillisecondsSinceEpoch(0),
);
factory MyWindSpeed.fromJson(Map<dynamic, dynamic> json) {
return MyWindSpeed(
speed: (json['speed'] ?? 0).toDouble(),
timestamp: DateTime.fromMillisecondsSinceEpoch(
(json['timestamp'] ?? 0) * 1000, // kalau dari Unix detik
).toLocal(),
timestamp: TimestampParser.parse(json['timestamp']),
);
}
}

View File

@ -1,21 +1,22 @@
// import 'models/models.dart';
/// file monitoring_repo.dart di "C:\App_project\klimatologi\packages\monitoring_repository\lib\src\monitoring_repo.dart"
import 'models/has_timestamp.dart';
abstract class MonitoringRepository {
// fungsi untuk ambil data berkali/streaming
Stream<T> getSensorStream<T>(
String path,
T Function(Map<dynamic, dynamic> json) mapper,
);
// fungsi untuk ambil sensor sekali
Future<T> getSensorSnapshot<T>(
String path,
T Function(Map<dynamic, dynamic> json) mapper,
);
// Fungsi untuk ambil riwayat (List)
Future<List<T>> getSensorHistory<T>(
Future<List<T>> getSensorHistory<T extends HasTimestamp>(
String path,
T Function(Map<dynamic, dynamic> json) mapper,
);
T Function(Map<dynamic, dynamic> json) mapper, {
String? orderByChild,
int limit = 500,
});
}

View File

@ -0,0 +1,46 @@
/// Mengubah berbagai format waktu Firebase DateTime.
///
/// Format yang didukung:
/// int Unix detik 1777807041 (anemometer)
/// int Unix milidetik 1777950497446 (sensor/atmospheric)
/// String "HH:MM:SS" "11:09:00" (Monitoring/evaporasi, intraday only)
/// String ISO 8601 "2025-03-05T14:26" (untuk masa depan)
class TimestampParser {
// Angka > ini dianggap milidetik (threshold: ~Nov 2001 dalam detik)
static const int _msThreshold = 10000000000;
static DateTime parse(dynamic raw) {
if (raw == null) return DateTime.now();
if (raw is int) {
if (raw <= 0) return DateTime.now();
if (raw > _msThreshold) {
return DateTime.fromMillisecondsSinceEpoch(raw).toLocal();
}
return DateTime.fromMillisecondsSinceEpoch(raw * 1000).toLocal();
}
if (raw is double) return parse(raw.toInt());
if (raw is String) {
// "HH:MM:SS" pakai tanggal hari ini (intraday only)
final m = RegExp(r'^(\d{1,2}):(\d{2}):(\d{2})$').firstMatch(raw);
if (m != null) {
final now = DateTime.now();
return DateTime(
now.year,
now.month,
now.day,
int.parse(m.group(1)!),
int.parse(m.group(2)!),
int.parse(m.group(3)!),
);
}
try {
return DateTime.parse(raw).toLocal();
} catch (_) {}
}
return DateTime.now();
}
}