update pembaruan time series mapper
This commit is contained in:
parent
e37fd688fb
commit
41e707d209
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'package:monitoring_repository/monitoring_repository.dart';
|
||||||
|
|
||||||
class TimeSeriesMapper {
|
class TimeSeriesMapper {
|
||||||
/// =========================
|
/// =========================
|
||||||
/// 📅 DAILY (24 JAM)
|
/// 📅 DAILY (24 JAM)
|
||||||
|
|
@ -114,4 +116,30 @@ class TimeSeriesMapper {
|
||||||
|
|
||||||
return result;
|
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));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
|
||||||
|
|
@ -35,26 +35,29 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
|
||||||
emit(state.copyWith(isLoading: true));
|
emit(state.copyWith(isLoading: true));
|
||||||
|
|
||||||
final history = await _repository.getSensorHistory(
|
final history = await _repository.getSensorHistory(
|
||||||
'evaporasi/history',
|
'Monitoring/History',
|
||||||
(json) => Evaporasi.fromJson(json),
|
(json) => Evaporasi.fromJson(json),
|
||||||
|
orderByChild: null, // waktu = string, tidak bisa orderByChild
|
||||||
|
limit: 500,
|
||||||
);
|
);
|
||||||
|
|
||||||
final dailyGraph = TimeSeriesMapper.toDaily(
|
final dailyGraph =
|
||||||
data: history,
|
TimeSeriesMapper.dailyFrom<Evaporasi>(history, (e) => e.evaporasi);
|
||||||
getTime: (e) => e.timestamp,
|
final dailyTempGraph =
|
||||||
getValue: (e) => e.evaporasi,
|
TimeSeriesMapper.dailyFrom<Evaporasi>(history, (e) => e.suhu);
|
||||||
);
|
final weeklyGraph =
|
||||||
|
TimeSeriesMapper.weeklyFrom<Evaporasi>(history, (e) => e.evaporasi);
|
||||||
final dailyTempGraph = TimeSeriesMapper.toDaily(
|
final monthlyGraph =
|
||||||
data: history,
|
TimeSeriesMapper.monthlyFrom<Evaporasi>(history, (e) => e.evaporasi);
|
||||||
getTime: (e) => e.timestamp,
|
final weeklyTemp =
|
||||||
getValue: (e) => e.suhu,
|
TimeSeriesMapper.weeklyFrom<Evaporasi>(history, (e) => e.suhu);
|
||||||
);
|
final monthlyTemp =
|
||||||
|
TimeSeriesMapper.monthlyFrom<Evaporasi>(history, (e) => e.suhu);
|
||||||
|
|
||||||
// Hitung status dari data terakhir history jika ada
|
// Hitung status dari data terakhir history jika ada
|
||||||
final lastValue = history.isNotEmpty ? history.last.evaporasi : 0.0;
|
final lastValue = history.isNotEmpty ? history.last.evaporasi : 0.0;
|
||||||
final (status, rain) = _computeWeatherStatus(lastValue);
|
final (status, rain) = _computeWeatherStatus(lastValue);
|
||||||
_emitEvaporasiAlert(status, rain, lastValue);
|
_emitEvaporasiAlert(status, lastValue);
|
||||||
|
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
history: history,
|
history: history,
|
||||||
|
|
@ -63,6 +66,10 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
|
||||||
weatherStatus: status,
|
weatherStatus: status,
|
||||||
willRain: rain,
|
willRain: rain,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
|
weeklyValues: weeklyGraph,
|
||||||
|
monthlyValues: monthlyGraph,
|
||||||
|
weeklyTemperatures: weeklyTemp,
|
||||||
|
monthlyTemperatures: monthlyTemp,
|
||||||
));
|
));
|
||||||
|
|
||||||
await _subscription?.cancel();
|
await _subscription?.cancel();
|
||||||
|
|
@ -88,7 +95,7 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
final (status, rain) = _computeWeatherStatus(event.data.evaporasi);
|
final (status, rain) = _computeWeatherStatus(event.data.evaporasi);
|
||||||
_emitEvaporasiAlert(status, rain, event.data.evaporasi);
|
_emitEvaporasiAlert(status, event.data.evaporasi);
|
||||||
|
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
currentValue: event.data.evaporasi,
|
currentValue: event.data.evaporasi,
|
||||||
|
|
@ -104,57 +111,11 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
|
||||||
/// =========================
|
/// =========================
|
||||||
/// 📊 PERIOD
|
/// 📊 PERIOD
|
||||||
/// =========================
|
/// =========================
|
||||||
Future<void> _onPeriodChanged(
|
void _onPeriodChanged(
|
||||||
EvaporasiPeriodChanged event,
|
EvaporasiPeriodChanged event,
|
||||||
Emitter<EvaporasiState> emit,
|
Emitter<EvaporasiState> emit,
|
||||||
) async {
|
) {
|
||||||
emit(state.copyWith(isLoading: true, selectedPeriod: event.period));
|
emit(state.copyWith(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,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -172,7 +133,7 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
|
||||||
return ('Buruk', true);
|
return ('Buruk', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _emitEvaporasiAlert(String status, bool willRain, double value) {
|
void _emitEvaporasiAlert(String status, double value) {
|
||||||
final AlertSeverity severity;
|
final AlertSeverity severity;
|
||||||
final String message;
|
final String message;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,17 @@ class EvaporasiState extends Equatable {
|
||||||
final String selectedPeriod;
|
final String selectedPeriod;
|
||||||
|
|
||||||
final List<double> dailyValues; // untuk grafik evaporasi
|
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> dailyTemperatures; // untuk grafik suhu
|
||||||
|
final List<double> weeklyTemperatures;
|
||||||
|
final List<double> monthlyTemperatures;
|
||||||
|
|
||||||
final List<Evaporasi> history;
|
final List<Evaporasi> history;
|
||||||
|
|
||||||
final String weatherStatus; // Baik / Sedang / Buruk
|
final String weatherStatus;
|
||||||
final bool willRain; // true jika status Sedang/Buruk
|
final bool willRain;
|
||||||
|
|
||||||
final bool isLoading;
|
final bool isLoading;
|
||||||
|
|
||||||
const EvaporasiState({
|
const EvaporasiState({
|
||||||
|
|
@ -21,7 +26,11 @@ class EvaporasiState extends Equatable {
|
||||||
this.waterLevel = 0.0,
|
this.waterLevel = 0.0,
|
||||||
this.selectedPeriod = "Hari Ini",
|
this.selectedPeriod = "Hari Ini",
|
||||||
this.dailyValues = const [],
|
this.dailyValues = const [],
|
||||||
|
this.weeklyValues = const [],
|
||||||
|
this.monthlyValues = const [],
|
||||||
this.dailyTemperatures = const [],
|
this.dailyTemperatures = const [],
|
||||||
|
this.weeklyTemperatures = const [],
|
||||||
|
this.monthlyTemperatures = const [],
|
||||||
this.history = const [],
|
this.history = const [],
|
||||||
this.weatherStatus = "Baik",
|
this.weatherStatus = "Baik",
|
||||||
this.willRain = false,
|
this.willRain = false,
|
||||||
|
|
@ -34,7 +43,11 @@ class EvaporasiState extends Equatable {
|
||||||
double? waterLevel,
|
double? waterLevel,
|
||||||
String? selectedPeriod,
|
String? selectedPeriod,
|
||||||
List<double>? dailyValues,
|
List<double>? dailyValues,
|
||||||
|
List<double>? weeklyValues,
|
||||||
|
List<double>? monthlyValues,
|
||||||
List<double>? dailyTemperatures,
|
List<double>? dailyTemperatures,
|
||||||
|
List<double>? weeklyTemperatures,
|
||||||
|
List<double>? monthlyTemperatures,
|
||||||
List<Evaporasi>? history,
|
List<Evaporasi>? history,
|
||||||
String? weatherStatus,
|
String? weatherStatus,
|
||||||
bool? willRain,
|
bool? willRain,
|
||||||
|
|
@ -46,7 +59,11 @@ class EvaporasiState extends Equatable {
|
||||||
waterLevel: waterLevel ?? this.waterLevel,
|
waterLevel: waterLevel ?? this.waterLevel,
|
||||||
selectedPeriod: selectedPeriod ?? this.selectedPeriod,
|
selectedPeriod: selectedPeriod ?? this.selectedPeriod,
|
||||||
dailyValues: dailyValues ?? this.dailyValues,
|
dailyValues: dailyValues ?? this.dailyValues,
|
||||||
|
weeklyValues: weeklyValues ?? this.weeklyValues,
|
||||||
|
monthlyValues: monthlyValues ?? this.monthlyValues,
|
||||||
dailyTemperatures: dailyTemperatures ?? this.dailyTemperatures,
|
dailyTemperatures: dailyTemperatures ?? this.dailyTemperatures,
|
||||||
|
weeklyTemperatures: weeklyTemperatures ?? this.weeklyTemperatures,
|
||||||
|
monthlyTemperatures: monthlyTemperatures ?? this.monthlyTemperatures,
|
||||||
history: history ?? this.history,
|
history: history ?? this.history,
|
||||||
weatherStatus: weatherStatus ?? this.weatherStatus,
|
weatherStatus: weatherStatus ?? this.weatherStatus,
|
||||||
willRain: willRain ?? this.willRain,
|
willRain: willRain ?? this.willRain,
|
||||||
|
|
@ -61,7 +78,11 @@ class EvaporasiState extends Equatable {
|
||||||
waterLevel,
|
waterLevel,
|
||||||
selectedPeriod,
|
selectedPeriod,
|
||||||
dailyValues,
|
dailyValues,
|
||||||
|
weeklyValues,
|
||||||
|
monthlyValues,
|
||||||
dailyTemperatures,
|
dailyTemperatures,
|
||||||
|
weeklyTemperatures,
|
||||||
|
monthlyTemperatures,
|
||||||
history,
|
history,
|
||||||
weatherStatus,
|
weatherStatus,
|
||||||
willRain,
|
willRain,
|
||||||
|
|
|
||||||
|
|
@ -48,32 +48,16 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
|
||||||
final history = await _repository.getSensorHistory(
|
final history = await _repository.getSensorHistory(
|
||||||
'anemometer/history',
|
'anemometer/history',
|
||||||
(json) => MyWindSpeed.fromJson(json),
|
(json) => MyWindSpeed.fromJson(json),
|
||||||
|
orderByChild: 'timestamp',
|
||||||
|
limit: 500,
|
||||||
);
|
);
|
||||||
|
|
||||||
// ✅ Ini saja yang benar
|
final dailyGraph =
|
||||||
final dailyGraph = TimeSeriesMapper.smooth(
|
TimeSeriesMapper.dailyFrom<MyWindSpeed>(history, (e) => e.speed);
|
||||||
TimeSeriesMapper.toDaily(
|
final weekly =
|
||||||
data: history,
|
TimeSeriesMapper.weeklyFrom<MyWindSpeed>(history, (e) => e.speed);
|
||||||
getTime: (e) => e.timestamp,
|
final monthly =
|
||||||
getValue: (e) => e.speed,
|
TimeSeriesMapper.monthlyFrom<MyWindSpeed>(history, (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,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Cek kondisi awal dari history
|
// Cek kondisi awal dari history
|
||||||
if (history.isNotEmpty) {
|
if (history.isNotEmpty) {
|
||||||
|
|
@ -141,48 +125,11 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
|
||||||
/// =========================
|
/// =========================
|
||||||
/// 📊 CHANGE PERIOD
|
/// 📊 CHANGE PERIOD
|
||||||
/// =========================
|
/// =========================
|
||||||
Future<void> _onPeriodChanged(
|
void _onPeriodChanged(
|
||||||
WindSpeedPeriodChanged event,
|
WindSpeedPeriodChanged event,
|
||||||
Emitter<WindSpeedState> emit,
|
Emitter<WindSpeedState> emit,
|
||||||
) async {
|
) {
|
||||||
emit(state.copyWith(selectedPeriod: event.period));
|
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
|
@override
|
||||||
|
|
|
||||||
|
|
@ -3,3 +3,4 @@ library monitoring_repository;
|
||||||
export 'src/monitoring_repo.dart';
|
export 'src/monitoring_repo.dart';
|
||||||
export 'src/firebase_monitoring_repo.dart';
|
export 'src/firebase_monitoring_repo.dart';
|
||||||
export 'src/models/models.dart';
|
export 'src/models/models.dart';
|
||||||
|
export 'src/utils/timestamp_parser.dart';
|
||||||
|
|
|
||||||
|
|
@ -1,72 +1,61 @@
|
||||||
// import 'dart:developer';
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
// import 'package:rxdart/rxdart.dart';
|
|
||||||
import 'package:firebase_database/firebase_database.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 {
|
class FirebaseMonitoringRepo implements MonitoringRepository {
|
||||||
final FirebaseDatabase _db = FirebaseDatabase.instance;
|
final FirebaseDatabase _db = FirebaseDatabase.instance;
|
||||||
|
|
||||||
// Satu fungsi untuk semua jenis sensor
|
|
||||||
// Kamu cukup masukkan "path" database-nya saja
|
|
||||||
@override
|
@override
|
||||||
Stream<T> getSensorStream<T>(
|
Stream<T> getSensorStream<T>(
|
||||||
String path,
|
String path,
|
||||||
T Function(Map<dynamic, dynamic> json) mapper,
|
T Function(Map<dynamic, dynamic> json) mapper,
|
||||||
) {
|
) {
|
||||||
/// === ambil data mentah dari firebase === ///
|
|
||||||
return _db.ref(path).onValue.map((event) {
|
return _db.ref(path).onValue.map((event) {
|
||||||
final Object? value = event.snapshot.value;
|
final value = event.snapshot.value;
|
||||||
|
return mapper(value is Map ? value : {});
|
||||||
if (value is Map) {
|
|
||||||
return mapper(value);
|
|
||||||
} else {
|
|
||||||
// Jika data kosong atau bukan Map, berikan Map kosong agar mapper tidak crash
|
|
||||||
return mapper({});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
// Jika ingin mengambil data sekali saja (bukan stream)
|
|
||||||
Future<T> getSensorSnapshot<T>(
|
Future<T> getSensorSnapshot<T>(
|
||||||
String path,
|
String path,
|
||||||
T Function(Map<dynamic, dynamic> json) mapper,
|
T Function(Map<dynamic, dynamic> json) mapper,
|
||||||
) async {
|
) async {
|
||||||
final snapshot = await _db.ref(path).get();
|
final snapshot = await _db.ref(path).get();
|
||||||
final data = snapshot.value as Map<dynamic, dynamic>? ?? {};
|
return mapper(snapshot.value as Map<dynamic, dynamic>? ?? {});
|
||||||
return mapper(data);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<T>> getSensorHistory<T>(
|
Future<List<T>> getSensorHistory<T extends HasTimestamp>(
|
||||||
String path,
|
String path,
|
||||||
T Function(Map<dynamic, dynamic> json) mapper,
|
T Function(Map<dynamic, dynamic> json) mapper, {
|
||||||
) async {
|
String? orderByChild,
|
||||||
|
int limit = 500,
|
||||||
|
}) async {
|
||||||
try {
|
try {
|
||||||
// Ambil data dari path history
|
Query query = _db.ref(path);
|
||||||
final snapshot = await _db.ref(path).get();
|
|
||||||
|
|
||||||
if (snapshot.exists && snapshot.value is Map) {
|
if (orderByChild != null) {
|
||||||
final Map<dynamic, dynamic> data = snapshot.value as Map;
|
query = query.orderByChild(orderByChild).limitToLast(limit);
|
||||||
|
} else {
|
||||||
final list = data.values.map((item) {
|
query = query.limitToLast(limit);
|
||||||
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);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
return list;
|
|
||||||
}
|
}
|
||||||
return [];
|
|
||||||
} catch (e) {
|
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;
|
||||||
|
} catch (_) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
class Evaporasi {
|
import 'has_timestamp.dart';
|
||||||
|
import '../utils/timestamp_parser.dart';
|
||||||
|
|
||||||
|
class Evaporasi implements HasTimestamp {
|
||||||
final double evaporasi;
|
final double evaporasi;
|
||||||
final double suhu;
|
final double suhu;
|
||||||
final double tinggiAir;
|
final double tinggiAir;
|
||||||
|
final String status;
|
||||||
|
|
||||||
|
@override
|
||||||
final DateTime timestamp;
|
final DateTime timestamp;
|
||||||
|
|
||||||
Evaporasi({
|
Evaporasi({
|
||||||
|
|
@ -9,6 +15,7 @@ class Evaporasi {
|
||||||
required this.suhu,
|
required this.suhu,
|
||||||
required this.tinggiAir,
|
required this.tinggiAir,
|
||||||
required this.timestamp,
|
required this.timestamp,
|
||||||
|
this.status = '',
|
||||||
});
|
});
|
||||||
|
|
||||||
static final empty = Evaporasi(
|
static final empty = Evaporasi(
|
||||||
|
|
@ -19,27 +26,22 @@ class Evaporasi {
|
||||||
);
|
);
|
||||||
|
|
||||||
factory Evaporasi.fromJson(Map<dynamic, dynamic> json) {
|
factory Evaporasi.fromJson(Map<dynamic, dynamic> json) {
|
||||||
final now = DateTime.now();
|
// ✅ Handle perbedaan field name antara history dan realtime:
|
||||||
DateTime timestamp = now;
|
// 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?;
|
// ✅ Prioritas: timestamp (Unix, kalau firmware sudah diupdate)
|
||||||
if (waktuStr != null) {
|
// → fallback ke waktu ("HH:MM:SS")
|
||||||
final parts = waktuStr.split(':');
|
final rawTime = json['timestamp'] ?? json['waktu'];
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Evaporasi(
|
return Evaporasi(
|
||||||
evaporasi: (json['evaporasi'] ?? 0).toDouble(),
|
evaporasi: (json['evaporasi'] ?? 0).toDouble(),
|
||||||
suhu: (json['suhu_air'] ?? 0).toDouble(),
|
suhu: suhu,
|
||||||
tinggiAir: (json['tinggi_air'] ?? 0).toDouble(),
|
tinggiAir: tinggi,
|
||||||
|
status: json['status'] ?? '',
|
||||||
/// 🔥 bikin timestamp dari jam & menit
|
timestamp: TimestampParser.parse(rawTime), // ✅ pakai TimestampParser
|
||||||
timestamp: timestamp,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
/// Semua model sensor wajib implement ini.
|
||||||
|
/// Memungkinkan sorting & TimeSeriesMapper yang generik.
|
||||||
|
abstract class HasTimestamp {
|
||||||
|
DateTime get timestamp;
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
export 'wind_speed.dart';
|
export 'wind_speed.dart';
|
||||||
export 'evaporasi.dart';
|
export 'evaporasi.dart';
|
||||||
export 'atmospheric_conditions.dart';
|
export 'atmospheric_conditions.dart';
|
||||||
|
export 'has_timestamp.dart';
|
||||||
abstract class HasTimestamp {
|
|
||||||
DateTime get timestamp;
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,23 @@
|
||||||
class MyWindSpeed {
|
import 'has_timestamp.dart';
|
||||||
double speed;
|
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});
|
MyWindSpeed({required this.speed, required this.timestamp});
|
||||||
|
|
||||||
static final empty = MyWindSpeed(
|
static final empty = MyWindSpeed(
|
||||||
speed: 0.0,
|
speed: 0.0,
|
||||||
|
|
||||||
timestamp: DateTime.fromMillisecondsSinceEpoch(0),
|
timestamp: DateTime.fromMillisecondsSinceEpoch(0),
|
||||||
);
|
);
|
||||||
|
|
||||||
factory MyWindSpeed.fromJson(Map<dynamic, dynamic> json) {
|
factory MyWindSpeed.fromJson(Map<dynamic, dynamic> json) {
|
||||||
return MyWindSpeed(
|
return MyWindSpeed(
|
||||||
speed: (json['speed'] ?? 0).toDouble(),
|
speed: (json['speed'] ?? 0).toDouble(),
|
||||||
|
timestamp: TimestampParser.parse(json['timestamp']),
|
||||||
timestamp: DateTime.fromMillisecondsSinceEpoch(
|
|
||||||
(json['timestamp'] ?? 0) * 1000, // kalau dari Unix detik
|
|
||||||
).toLocal(),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
abstract class MonitoringRepository {
|
||||||
// fungsi untuk ambil data berkali/streaming
|
|
||||||
Stream<T> getSensorStream<T>(
|
Stream<T> getSensorStream<T>(
|
||||||
String path,
|
String path,
|
||||||
T Function(Map<dynamic, dynamic> json) mapper,
|
T Function(Map<dynamic, dynamic> json) mapper,
|
||||||
);
|
);
|
||||||
|
|
||||||
// fungsi untuk ambil sensor sekali
|
|
||||||
Future<T> getSensorSnapshot<T>(
|
Future<T> getSensorSnapshot<T>(
|
||||||
String path,
|
String path,
|
||||||
T Function(Map<dynamic, dynamic> json) mapper,
|
T Function(Map<dynamic, dynamic> json) mapper,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Fungsi untuk ambil riwayat (List)
|
Future<List<T>> getSensorHistory<T extends HasTimestamp>(
|
||||||
Future<List<T>> getSensorHistory<T>(
|
|
||||||
String path,
|
String path,
|
||||||
T Function(Map<dynamic, dynamic> json) mapper,
|
T Function(Map<dynamic, dynamic> json) mapper, {
|
||||||
);
|
String? orderByChild,
|
||||||
|
int limit = 500,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue