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 {
|
||||
/// =========================
|
||||
/// 📅 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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
||||
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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 [];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 'evaporasi.dart';
|
||||
export 'atmospheric_conditions.dart';
|
||||
|
||||
abstract class HasTimestamp {
|
||||
DateTime get timestamp;
|
||||
}
|
||||
export 'has_timestamp.dart';
|
||||
|
|
|
|||
|
|
@ -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']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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