import 'package:dio/dio.dart'; import '../models/sensor_model.dart'; import '../core/api_config.dart'; import 'iot_service.dart'; /// Implementasi [IotService] yang berkomunikasi ke Backend Express melalui REST API. /// Menangani pemrosesan data mentah sensor, pemfilteran nilai kosong (null), serta riwayat grafik. class ApiIotService implements IotService { final Dio _dio; final String deviceId; ApiIotService(this.deviceId, [Dio? dio]) : _dio = dio ?? Dio(); @override Future fetchSensorData() async { try { // Ambil 30 data riwayat terbaru dari backend. // Hal ini dilakukan agar kita bisa memfilter dan melewati data log yang nilainya null (misalnya log perubahan relay saja). final response = await _dio.get('${ApiConfig.baseUrl}/history/$deviceId?limit=30'); if (response.statusCode == 200) { final List data = response.data; if (data.isEmpty) { return SensorModel(temperature: 0, humidity: 0, timestamp: DateTime.now()); } final firstItem = data.first; // Mencari entri pertama yang memiliki nilai sensor (suhu & kelembapan) yang valid (bukan null) final validItem = data.firstWhere( (item) => item['temperature'] != null && item['humidity'] != null, orElse: () => null, ); if (validItem != null) { return SensorModel( temperature: (validItem['temperature'] as num).toDouble(), humidity: (validItem['humidity'] as num).toDouble(), relayOn: firstItem['relay_state'] as bool? ?? false, mode: firstItem['mode'] as String? ?? 'auto', timestamp: DateTime.parse(firstItem['created_at']).toLocal(), ); } else { // Jika belum ada data sensor yang valid, kembalikan nilai placeholder (0) namun status relay & mode tetap valid return SensorModel( temperature: 0, humidity: 0, relayOn: firstItem['relay_state'] as bool? ?? false, mode: firstItem['mode'] as String? ?? 'auto', timestamp: DateTime.parse(firstItem['created_at']).toLocal() ); } } else { throw Exception('Failed to fetch sensor data: ${response.statusCode}'); } } catch (e) { throw Exception('Error fetching data: $e'); } } /// Mengambil data histori sensor secara lengkap untuk kebutuhan pembuatan grafik real-time. /// [limit] menentukan jumlah maksimal entri data yang diambil. Future> fetchHistory({int limit = 100}) async { try { final response = await _dio.get('${ApiConfig.baseUrl}/history/$deviceId?limit=$limit'); if (response.statusCode == 200) { final List data = response.data; // Data dari API diurutkan DESC (terbaru di atas), // kita balik urutannya (reverse) agar berurutan ASC (terlama ke terbaru) untuk penggambaran grafik. // Lewati entri yang nilai suhu/kelembapannya null. final validData = data.where( (item) => item['temperature'] != null && item['humidity'] != null, ).toList(); return validData.reversed.map((item) => SensorModel( temperature: (item['temperature'] as num).toDouble(), humidity: (item['humidity'] as num).toDouble(), relayOn: item['relay_state'] as bool? ?? false, mode: item['mode'] as String? ?? 'auto', timestamp: DateTime.parse(item['created_at']).toLocal(), )).toList(); } else { throw Exception('Failed to fetch history: ${response.statusCode}'); } } catch (e) { throw Exception('Error fetching history: $e'); } } /// Mengambil riwayat data agregasi harian (rata-rata harian) untuk grafik bulanan. /// [days] menentukan berapa hari data ke belakang yang akan diambil. Future>> fetchDailyHistory({int days = 30}) async { try { final response = await _dio.get('${ApiConfig.baseUrl}/history/$deviceId/daily?days=$days'); if (response.statusCode == 200) { final List data = response.data; return data.cast>(); } else { throw Exception('Failed to fetch daily history: ${response.statusCode}'); } } catch (e) { throw Exception('Error fetching daily history: $e'); } } /// Mengambil riwayat data agregasi per jam (rata-rata per jam) untuk grafik harian. /// [days] menentukan rentang hari ke belakang data yang akan diambil. Future>> fetchHourlyHistory({int days = 7}) async { try { final response = await _dio.get('${ApiConfig.baseUrl}/history/$deviceId/hourly?days=$days'); if (response.statusCode == 200) { final List data = response.data; return data.cast>(); } else { throw Exception('Failed to fetch hourly history: ${response.statusCode}'); } } catch (e) { throw Exception('Error fetching hourly history: $e'); } } /// Mengambil status keaktifan perangkat (is_online, last_seen) dari backend. Future> fetchDeviceStatus() async { try { final response = await _dio.get('${ApiConfig.baseUrl}/device/status/$deviceId'); if (response.statusCode == 200) { return response.data as Map; } else { throw Exception('Failed to fetch device status: ${response.statusCode}'); } } catch (e) { throw Exception('Error fetching device status: $e'); } } }