This commit is contained in:
e32232340-Nadif 2026-04-19 07:39:04 +07:00
parent 828645da5b
commit 95b1759344
5 changed files with 345 additions and 108 deletions

View File

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

View File

@ -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];
}

View File

@ -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];
}

View File

@ -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];
}