This commit is contained in:
Mochamad ongki ramadani 2026-05-08 16:13:23 +07:00
commit fff7f8b5db
14 changed files with 820 additions and 137 deletions

BIN
analyze_log.txt Normal file

Binary file not shown.

File diff suppressed because one or more lines are too long

BIN
build_log.txt Normal file

Binary file not shown.

BIN
build_log2.txt Normal file

Binary file not shown.

View File

@ -1,3 +1,5 @@
import 'package:monitoring_repository/monitoring_repository.dart';
class TimeSeriesMapper {
/// =========================
/// 📅 DAILY (24 JAM)
@ -142,4 +144,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

@ -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);
}
return 0;
});
return list;
if (orderByChild != null) {
query = query.orderByChild(orderByChild).limitToLast(limit);
} else {
query = query.limitToLast(limit);
}
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 [];
}
}

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,3 +1,4 @@
export 'wind_speed.dart';
export 'evaporasi.dart';
export 'atmospheric_conditions.dart';
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();
}
}