oke
This commit is contained in:
parent
828645da5b
commit
95b1759344
|
|
@ -0,0 +1,82 @@
|
|||
class TimeSeriesMapper {
|
||||
/// =========================
|
||||
/// 📅 DAILY (24 JAM)
|
||||
/// =========================
|
||||
static List<double> toDaily<T>({
|
||||
required List<T> data,
|
||||
required DateTime Function(T) getTime,
|
||||
required double Function(T) getValue,
|
||||
}) {
|
||||
final now = DateTime.now();
|
||||
final slots = List<double>.filled(24, 0.0);
|
||||
|
||||
for (final item in data) {
|
||||
final time = getTime(item);
|
||||
|
||||
if (_isSameDay(time, now)) {
|
||||
slots[time.hour] = getValue(item);
|
||||
}
|
||||
}
|
||||
|
||||
return slots;
|
||||
}
|
||||
|
||||
/// =========================
|
||||
/// 📅 WEEKLY (7 HARI)
|
||||
/// =========================
|
||||
static List<double> toWeekly<T>({
|
||||
required List<T> data,
|
||||
required DateTime Function(T) getTime,
|
||||
required double Function(T) getValue,
|
||||
}) {
|
||||
final now = DateTime.now();
|
||||
final slots = List<double>.filled(7, 0.0);
|
||||
|
||||
DateTime startOfWeek =
|
||||
now.subtract(Duration(days: now.weekday - 1));
|
||||
startOfWeek = DateTime(
|
||||
startOfWeek.year, startOfWeek.month, startOfWeek.day);
|
||||
|
||||
for (final item in data) {
|
||||
final time = getTime(item);
|
||||
|
||||
if (time.isAfter(startOfWeek)) {
|
||||
slots[time.weekday - 1] = getValue(item);
|
||||
}
|
||||
}
|
||||
|
||||
return slots;
|
||||
}
|
||||
|
||||
/// =========================
|
||||
/// 📅 MONTHLY
|
||||
/// =========================
|
||||
static List<double> toMonthly<T>({
|
||||
required List<T> data,
|
||||
required DateTime Function(T) getTime,
|
||||
required double Function(T) getValue,
|
||||
}) {
|
||||
final now = DateTime.now();
|
||||
final daysInMonth = DateTime(now.year, now.month + 1, 0).day;
|
||||
final slots = List<double>.filled(daysInMonth, 0.0);
|
||||
|
||||
for (final item in data) {
|
||||
final time = getTime(item);
|
||||
|
||||
if (time.month == now.month && time.year == now.year) {
|
||||
slots[time.day - 1] = getValue(item);
|
||||
}
|
||||
}
|
||||
|
||||
return slots;
|
||||
}
|
||||
|
||||
/// =========================
|
||||
/// 🧠 HELPER
|
||||
/// =========================
|
||||
static bool _isSameDay(DateTime a, DateTime b) {
|
||||
return a.year == b.year &&
|
||||
a.month == b.month &&
|
||||
a.day == b.day;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
import 'dart:async';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:monitoring_repository/monitoring_repository.dart';
|
||||
|
||||
import '../../../core/utils/time_series_mapper.dart';
|
||||
|
||||
part 'evaporasi_event.dart';
|
||||
part 'evaporasi_state.dart';
|
||||
|
||||
class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
|
||||
final MonitoringRepository _repository;
|
||||
StreamSubscription<Evaporasi>? _subscription;
|
||||
|
||||
EvaporasiBloc({required MonitoringRepository repository})
|
||||
: _repository = repository,
|
||||
super(const EvaporasiState()) {
|
||||
|
||||
on<WatchEvaporasiStarted>(_onStarted);
|
||||
on<_EvaporasiRealtimeUpdated>(_onRealtimeUpdated);
|
||||
on<EvaporasiPeriodChanged>(_onPeriodChanged);
|
||||
}
|
||||
|
||||
/// =========================
|
||||
/// 🚀 START
|
||||
/// =========================
|
||||
Future<void> _onStarted(
|
||||
WatchEvaporasiStarted event,
|
||||
Emitter<EvaporasiState> emit,
|
||||
) async {
|
||||
|
||||
emit(state.copyWith(isLoading: true));
|
||||
|
||||
final history = await _repository.getSensorHistory(
|
||||
'evaporasi/history',
|
||||
(json) => Evaporasi.fromJson(json),
|
||||
);
|
||||
|
||||
final dailyGraph = TimeSeriesMapper.toDaily(
|
||||
data: history,
|
||||
getTime: (e) => e.timestamp,
|
||||
getValue: (e) => e.value, // ⚠️ sesuaikan nama field
|
||||
);
|
||||
|
||||
emit(state.copyWith(
|
||||
history: history,
|
||||
dailyValues: dailyGraph,
|
||||
isLoading: false,
|
||||
));
|
||||
|
||||
await _subscription?.cancel();
|
||||
|
||||
_subscription = _repository
|
||||
.getSensorStream(
|
||||
'evaporasi/realtime',
|
||||
(json) => Evaporasi.fromJson(json),
|
||||
)
|
||||
.listen((data) {
|
||||
add(_EvaporasiRealtimeUpdated(data));
|
||||
});
|
||||
}
|
||||
|
||||
/// =========================
|
||||
/// ⚡ REALTIME
|
||||
/// =========================
|
||||
void _onRealtimeUpdated(
|
||||
_EvaporasiRealtimeUpdated event,
|
||||
Emitter<EvaporasiState> emit,
|
||||
) {
|
||||
|
||||
final updated = List<double>.from(state.dailyValues);
|
||||
|
||||
final index = DateTime.now().hour;
|
||||
|
||||
if (index < updated.length) {
|
||||
updated[index] = event.data.value; // ⚠️ sesuaikan field
|
||||
}
|
||||
|
||||
emit(state.copyWith(
|
||||
currentValue: event.data.value,
|
||||
dailyValues: updated,
|
||||
));
|
||||
}
|
||||
|
||||
/// =========================
|
||||
/// 📊 PERIOD
|
||||
/// =========================
|
||||
Future<void> _onPeriodChanged(
|
||||
EvaporasiPeriodChanged event,
|
||||
Emitter<EvaporasiState> emit,
|
||||
) async {
|
||||
|
||||
emit(state.copyWith(isLoading: true, selectedPeriod: event.period));
|
||||
|
||||
final history = state.history;
|
||||
|
||||
List<double> updated;
|
||||
|
||||
if (event.period == "Minggu Ini") {
|
||||
updated = TimeSeriesMapper.toWeekly(
|
||||
data: history,
|
||||
getTime: (e) => e.timestamp,
|
||||
getValue: (e) => e.value,
|
||||
);
|
||||
} else if (event.period == "Bulan Ini") {
|
||||
updated = TimeSeriesMapper.toMonthly(
|
||||
data: history,
|
||||
getTime: (e) => e.timestamp,
|
||||
getValue: (e) => e.value,
|
||||
);
|
||||
} else {
|
||||
updated = TimeSeriesMapper.toDaily(
|
||||
data: history,
|
||||
getTime: (e) => e.timestamp,
|
||||
getValue: (e) => e.value,
|
||||
);
|
||||
}
|
||||
|
||||
emit(state.copyWith(
|
||||
dailyValues: updated,
|
||||
isLoading: false,
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _subscription?.cancel();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
/// INTERNAL EVENT
|
||||
class _EvaporasiRealtimeUpdated extends EvaporasiEvent {
|
||||
final Evaporasi data;
|
||||
|
||||
const _EvaporasiRealtimeUpdated(this.data);
|
||||
|
||||
@override
|
||||
List<Object> get props => [data];
|
||||
}
|
||||
|
|
@ -3,138 +3,149 @@ import 'package:bloc/bloc.dart';
|
|||
import 'package:equatable/equatable.dart';
|
||||
import 'package:monitoring_repository/monitoring_repository.dart';
|
||||
import 'package:bloc_concurrency/bloc_concurrency.dart';
|
||||
import '../../../../core/utils/time_series_mapper.dart';
|
||||
|
||||
part 'wind_speed_event.dart';
|
||||
part 'wind_speed_state.dart';
|
||||
|
||||
class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
|
||||
final MonitoringRepository _repository;
|
||||
StreamSubscription<MyWindSpeed>? _subscription;
|
||||
|
||||
WindSpeedBloc({required MonitoringRepository repository})
|
||||
: _repository = repository,
|
||||
super(const WindSpeedState()) {
|
||||
// Handler saat aplikasi minta mulai monitoring
|
||||
on<WatchWindSpeedStarted>(
|
||||
(event, emit) async {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
|
||||
// 1. Ambil semua history
|
||||
final history = await _repository.getSensorHistory(
|
||||
'anemometer/history', (json) => MyWindSpeed.fromJson(json));
|
||||
/// 🔥 START MONITORING
|
||||
on<WatchWindSpeedStarted>(_onStarted, transformer: restartable());
|
||||
|
||||
// 2. Petakan ke 24 jam (Harian)
|
||||
final List<double> dailyGraph = _mapHistoryToDaily(history);
|
||||
/// 🔥 REALTIME UPDATE
|
||||
on<_WindSpeedRealtimeUpdated>(_onRealtimeUpdated);
|
||||
|
||||
emit(state.copyWith(
|
||||
dailySpeeds: dailyGraph,
|
||||
isLoading: false,
|
||||
));
|
||||
/// 🔥 CHANGE PERIOD
|
||||
on<WindSpeedPeriodChanged>(_onPeriodChanged);
|
||||
}
|
||||
|
||||
// 2. Monitoring Real-time
|
||||
await emit.forEach<MyWindSpeed>(
|
||||
_repository.getSensorStream(
|
||||
'anemometer/realtime', (json) => MyWindSpeed.fromJson(json)),
|
||||
onData: (data) {
|
||||
// 1. Ambil list kecepatan yang ada saat ini di state
|
||||
final updatedSpeeds = List<double>.from(state.dailySpeeds);
|
||||
/// =========================
|
||||
/// 🚀 START
|
||||
/// =========================
|
||||
Future<void> _onStarted(
|
||||
WatchWindSpeedStarted event,
|
||||
Emitter<WindSpeedState> emit,
|
||||
) async {
|
||||
|
||||
// 2. Tentukan di index mana data ini harus masuk (misal berdasarkan menit saat ini)
|
||||
final int index = DateTime.now().minute;
|
||||
emit(state.copyWith(isLoading: true));
|
||||
|
||||
// 3. Update nilai di index tersebut
|
||||
if (index < updatedSpeeds.length) {
|
||||
updatedSpeeds[index] = data.speed;
|
||||
}
|
||||
|
||||
return state.copyWith(
|
||||
currentSpeed: data.speed,
|
||||
dailySpeeds: updatedSpeeds,
|
||||
isLoading: false,
|
||||
);
|
||||
},
|
||||
onError: (error, stackTrace) => state.copyWith(isLoading: false),
|
||||
); // emit forEach
|
||||
},
|
||||
transformer: restartable(),
|
||||
/// 1. Ambil history SEKALI
|
||||
final history = await _repository.getSensorHistory(
|
||||
'anemometer/history',
|
||||
(json) => MyWindSpeed.fromJson(json),
|
||||
);
|
||||
|
||||
on<WindSpeedPeriodChanged>((event, emit) async {
|
||||
emit(state.copyWith(selectedPeriod: event.period));
|
||||
// Ambil ulang history dari database
|
||||
final history = await _repository.getSensorHistory(
|
||||
'anemometer/history', (json) => MyWindSpeed.fromJson(json));
|
||||
List<double> updatedGraph;
|
||||
/// 2. Mapping default (harian)
|
||||
final dailyGraph = TimeSeriesMapper.toDaily(
|
||||
data: history,
|
||||
getTime: (e) => e.timestamp,
|
||||
getValue: (e) => e.speed,
|
||||
);
|
||||
|
||||
// Pilih mapper berdasarkan pilihan user
|
||||
if (event.period == "Minggu Ini") {
|
||||
updatedGraph = _mapHistoryToWeekly(history);
|
||||
} else if (event.period == "Bulan Ini") {
|
||||
updatedGraph = _mapHistoryToMonthly(history);
|
||||
} else {
|
||||
updatedGraph = _mapHistoryToDaily(history);
|
||||
}
|
||||
emit(state.copyWith(
|
||||
history: history,
|
||||
dailySpeeds: dailyGraph,
|
||||
isLoading: false,
|
||||
));
|
||||
|
||||
emit(state.copyWith(
|
||||
dailySpeeds: updatedGraph,
|
||||
isLoading: false,
|
||||
));
|
||||
/// 3. Start realtime stream (manual subscription)
|
||||
await _subscription?.cancel();
|
||||
|
||||
_subscription = _repository
|
||||
.getSensorStream(
|
||||
'anemometer/realtime',
|
||||
(json) => MyWindSpeed.fromJson(json),
|
||||
)
|
||||
.listen((data) {
|
||||
add(_WindSpeedRealtimeUpdated(data));
|
||||
});
|
||||
}
|
||||
|
||||
/// =========================
|
||||
/// ⚡ REALTIME UPDATE
|
||||
/// =========================
|
||||
void _onRealtimeUpdated(
|
||||
_WindSpeedRealtimeUpdated event,
|
||||
Emitter<WindSpeedState> emit,
|
||||
) {
|
||||
|
||||
final updatedSpeeds = List<double>.from(state.dailySpeeds);
|
||||
|
||||
/// 🔥 FIX: pakai JAM bukan menit
|
||||
final int index = DateTime.now().hour;
|
||||
|
||||
if (index < updatedSpeeds.length) {
|
||||
updatedSpeeds[index] = event.data.speed;
|
||||
}
|
||||
|
||||
emit(state.copyWith(
|
||||
currentSpeed: event.data.speed,
|
||||
dailySpeeds: updatedSpeeds,
|
||||
));
|
||||
}
|
||||
|
||||
/// =========================
|
||||
/// 📊 CHANGE PERIOD
|
||||
/// =========================
|
||||
Future<void> _onPeriodChanged(
|
||||
WindSpeedPeriodChanged event,
|
||||
Emitter<WindSpeedState> emit,
|
||||
) async {
|
||||
|
||||
emit(state.copyWith(isLoading: true, selectedPeriod: event.period));
|
||||
|
||||
final history = state.history;
|
||||
|
||||
List<double> updatedGraph;
|
||||
|
||||
if (event.period == "Minggu Ini") {
|
||||
updatedGraph = TimeSeriesMapper.toWeekly(
|
||||
data: history,
|
||||
getTime: (e) => e.timestamp,
|
||||
getValue: (e) => e.speed,
|
||||
);
|
||||
} else if (event.period == "Bulan Ini") {
|
||||
updatedGraph = TimeSeriesMapper.toMonthly(
|
||||
data: history,
|
||||
getTime: (e) => e.timestamp,
|
||||
getValue: (e) => e.speed,
|
||||
);
|
||||
} else {
|
||||
updatedGraph = TimeSeriesMapper.toDaily(
|
||||
data: history,
|
||||
getTime: (e) => e.timestamp,
|
||||
getValue: (e) => e.speed,
|
||||
);
|
||||
}
|
||||
|
||||
emit(state.copyWith(
|
||||
dailySpeeds: updatedGraph,
|
||||
isLoading: false,
|
||||
));
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() => super.close();
|
||||
}
|
||||
|
||||
List<double> _mapHistoryToDaily(List<MyWindSpeed> history) {
|
||||
// Buat list 24 angka nol (representasi jam 00:00 - 23:00)
|
||||
List<double> slots = List.generate(24, (_) => 0.0); // mode asli
|
||||
// Kita buat 60 slot (representasi menit 0-59) agar tiap menit kelihatan titik baru
|
||||
// List<double> slots = List.generate(60, (_) => 0.0); // mode percobaan
|
||||
|
||||
for (var item in history) {
|
||||
// Ubah timestamp UTC ke jam lokal
|
||||
DateTime date = item.timestamp; // sudah dateTime dari model
|
||||
|
||||
// Jika data ini adalah data hari ini, masukkan ke slot jamnya
|
||||
if (date.day == DateTime.now().day) {
|
||||
slots[date.hour] = item.speed;
|
||||
} // mode asli
|
||||
// pakai menit untuk percobaan (date.minute) sebagai index supaya kelihatan pergerakannya
|
||||
// if (date.day == DateTime.now().day) {
|
||||
// slots[date.minute] = item.speed;
|
||||
// } // mode percobaan/testing
|
||||
Future<void> close() async {
|
||||
await _subscription?.cancel();
|
||||
return super.close();
|
||||
}
|
||||
return slots;
|
||||
}
|
||||
|
||||
// Mapper Mingguan (7 Slot: Senin - Minggu)
|
||||
List<double> _mapHistoryToWeekly(List<MyWindSpeed> history) {
|
||||
List<double> slots = List.generate(7, (_) => 0.0);
|
||||
final now = DateTime.now();
|
||||
/// =========================
|
||||
/// 🔒 INTERNAL EVENT (PRIVATE)
|
||||
/// =========================
|
||||
class _WindSpeedRealtimeUpdated extends WindSpeedEvent {
|
||||
final MyWindSpeed data;
|
||||
|
||||
// Mencari awal minggu ini (Senin)
|
||||
DateTime startOfWeek = now.subtract(Duration(days: now.weekday - 1));
|
||||
startOfWeek = DateTime(startOfWeek.year, startOfWeek.month, startOfWeek.day);
|
||||
const _WindSpeedRealtimeUpdated(this.data);
|
||||
|
||||
for (var item in history) {
|
||||
if (item.timestamp.isAfter(startOfWeek)) {
|
||||
// index 0 = Senin, dst.
|
||||
slots[item.timestamp.weekday - 1] = item.speed;
|
||||
}
|
||||
}
|
||||
return slots;
|
||||
}
|
||||
|
||||
// Mapper Bulanan (Sesuai jumlah hari di bulan ini)
|
||||
List<double> _mapHistoryToMonthly(List<MyWindSpeed> history) {
|
||||
final now = DateTime.now();
|
||||
final daysInMonth = DateTime(now.year, now.month + 1, 0).day;
|
||||
List<double> slots = List.generate(daysInMonth, (_) => 0.0);
|
||||
|
||||
for (var item in history) {
|
||||
if (item.timestamp.month == now.month && item.timestamp.year == now.year) {
|
||||
// index 0 = Tanggal 1, dst.
|
||||
slots[item.timestamp.day - 1] = item.speed;
|
||||
}
|
||||
}
|
||||
return slots;
|
||||
@override
|
||||
List<Object> get props => [data];
|
||||
}
|
||||
|
|
@ -5,12 +5,14 @@ class WindSpeedState extends Equatable {
|
|||
final String selectedPeriod;
|
||||
final List<double> dailySpeeds; // Untuk grafik
|
||||
final bool isLoading;
|
||||
final List<MyWindSpeed> history;
|
||||
|
||||
const WindSpeedState({
|
||||
this.currentSpeed = 0.0,
|
||||
this.selectedPeriod = "Hari Ini",
|
||||
this.dailySpeeds = const [],
|
||||
this.isLoading = true,
|
||||
this.history = const [],
|
||||
});
|
||||
|
||||
WindSpeedState copyWith({
|
||||
|
|
@ -18,16 +20,18 @@ class WindSpeedState extends Equatable {
|
|||
String? selectedPeriod,
|
||||
List<double>? dailySpeeds,
|
||||
bool? isLoading,
|
||||
List<MyWindSpeed>? history,
|
||||
}) {
|
||||
return WindSpeedState(
|
||||
currentSpeed: currentSpeed ?? this.currentSpeed,
|
||||
selectedPeriod: selectedPeriod ?? this.selectedPeriod,
|
||||
dailySpeeds: dailySpeeds ?? this.dailySpeeds,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
history: history ?? this.history,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object> get props =>
|
||||
[currentSpeed, selectedPeriod, dailySpeeds, isLoading];
|
||||
[currentSpeed, selectedPeriod, dailySpeeds, isLoading, history];
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue