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 { class TimeSeriesMapper {
/// ========================= /// =========================
/// 📅 DAILY (24 JAM) /// 📅 DAILY (24 JAM)
@ -142,4 +144,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));
} }

View File

@ -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

View File

@ -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';

View File

@ -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;
});
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 list;
} } catch (_) {
return [];
} catch (e) {
return []; 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 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,
); );
} }
} }

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 'wind_speed.dart';
export 'evaporasi.dart'; export 'evaporasi.dart';
export 'atmospheric_conditions.dart'; export 'atmospheric_conditions.dart';
export 'has_timestamp.dart';

View File

@ -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(),
); );
} }
} }

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 { 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,
});
} }

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();
}
}