Revert "tes"
This reverts commitfff7f8b5db, reversing changes made to2675ed7bd4.
This commit is contained in:
parent
fff7f8b5db
commit
6a6b6a43cd
BIN
analyze_log.txt
BIN
analyze_log.txt
Binary file not shown.
File diff suppressed because one or more lines are too long
BIN
build_log.txt
BIN
build_log.txt
Binary file not shown.
BIN
build_log2.txt
BIN
build_log2.txt
Binary file not shown.
|
|
@ -1,5 +1,3 @@
|
||||||
import 'package:monitoring_repository/monitoring_repository.dart';
|
|
||||||
|
|
||||||
class TimeSeriesMapper {
|
class TimeSeriesMapper {
|
||||||
/// =========================
|
/// =========================
|
||||||
/// 📅 DAILY (24 JAM)
|
/// 📅 DAILY (24 JAM)
|
||||||
|
|
@ -144,30 +142,4 @@ 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));
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -48,16 +48,32 @@ 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,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
final dailyGraph =
|
// ✅ Ini saja yang benar
|
||||||
TimeSeriesMapper.dailyFrom<MyWindSpeed>(history, (e) => e.speed);
|
final dailyGraph = TimeSeriesMapper.smooth(
|
||||||
final weekly =
|
TimeSeriesMapper.toDaily(
|
||||||
TimeSeriesMapper.weeklyFrom<MyWindSpeed>(history, (e) => e.speed);
|
data: history,
|
||||||
final monthly =
|
getTime: (e) => e.timestamp,
|
||||||
TimeSeriesMapper.monthlyFrom<MyWindSpeed>(history, (e) => e.speed);
|
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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// Cek kondisi awal dari history
|
// Cek kondisi awal dari history
|
||||||
if (history.isNotEmpty) {
|
if (history.isNotEmpty) {
|
||||||
|
|
@ -125,11 +141,48 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
|
||||||
/// =========================
|
/// =========================
|
||||||
/// 📊 CHANGE PERIOD
|
/// 📊 CHANGE PERIOD
|
||||||
/// =========================
|
/// =========================
|
||||||
void _onPeriodChanged(
|
Future<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,4 +3,3 @@ 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,61 +1,72 @@
|
||||||
|
// 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 'monitoring_repo.dart';
|
import 'package:monitoring_repository/monitoring_repository.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 value = event.snapshot.value;
|
final Object? 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();
|
||||||
return mapper(snapshot.value as Map<dynamic, dynamic>? ?? {});
|
final data = snapshot.value as Map<dynamic, dynamic>? ?? {};
|
||||||
|
return mapper(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
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,
|
) async {
|
||||||
int limit = 500,
|
|
||||||
}) async {
|
|
||||||
try {
|
try {
|
||||||
Query query = _db.ref(path);
|
// Ambil data dari path history
|
||||||
|
final snapshot = await _db.ref(path).get();
|
||||||
|
|
||||||
if (orderByChild != null) {
|
if (snapshot.exists && snapshot.value is Map) {
|
||||||
query = query.orderByChild(orderByChild).limitToLast(limit);
|
final Map<dynamic, dynamic> data = snapshot.value as Map;
|
||||||
} else {
|
|
||||||
query = query.limitToLast(limit);
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
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 [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,7 @@
|
||||||
import 'has_timestamp.dart';
|
class Evaporasi {
|
||||||
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({
|
||||||
|
|
@ -15,7 +9,6 @@ class Evaporasi implements HasTimestamp {
|
||||||
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(
|
||||||
|
|
@ -26,22 +19,27 @@ class Evaporasi implements HasTimestamp {
|
||||||
);
|
);
|
||||||
|
|
||||||
factory Evaporasi.fromJson(Map<dynamic, dynamic> json) {
|
factory Evaporasi.fromJson(Map<dynamic, dynamic> json) {
|
||||||
// ✅ Handle perbedaan field name antara history dan realtime:
|
final now = DateTime.now();
|
||||||
// History path : suhu, tinggi, evaporasi, waktu
|
DateTime timestamp = now;
|
||||||
// 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();
|
|
||||||
|
|
||||||
// ✅ Prioritas: timestamp (Unix, kalau firmware sudah diupdate)
|
final waktuStr = json['waktu'] as String?;
|
||||||
// → fallback ke waktu ("HH:MM:SS")
|
if (waktuStr != null) {
|
||||||
final rawTime = json['timestamp'] ?? json['waktu'];
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Evaporasi(
|
return Evaporasi(
|
||||||
evaporasi: (json['evaporasi'] ?? 0).toDouble(),
|
evaporasi: (json['evaporasi'] ?? 0).toDouble(),
|
||||||
suhu: suhu,
|
suhu: (json['suhu_air'] ?? 0).toDouble(),
|
||||||
tinggiAir: tinggi,
|
tinggiAir: (json['tinggi_air'] ?? 0).toDouble(),
|
||||||
status: json['status'] ?? '',
|
|
||||||
timestamp: TimestampParser.parse(rawTime), // ✅ pakai TimestampParser
|
/// 🔥 bikin timestamp dari jam & menit
|
||||||
|
timestamp: timestamp,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
/// Semua model sensor wajib implement ini.
|
|
||||||
/// Memungkinkan sorting & TimeSeriesMapper yang generik.
|
|
||||||
abstract class HasTimestamp {
|
|
||||||
DateTime get timestamp;
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
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';
|
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,23 @@
|
||||||
import 'has_timestamp.dart';
|
class MyWindSpeed {
|
||||||
import '../utils/timestamp_parser.dart';
|
double speed;
|
||||||
|
|
||||||
class MyWindSpeed implements HasTimestamp {
|
DateTime timestamp;
|
||||||
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,22 +1,21 @@
|
||||||
/// file monitoring_repo.dart di "C:\App_project\klimatologi\packages\monitoring_repository\lib\src\monitoring_repo.dart"
|
// import 'models/models.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,
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<List<T>> getSensorHistory<T extends HasTimestamp>(
|
// Fungsi untuk ambil riwayat (List)
|
||||||
|
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,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
/// 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