import 'dart:convert'; import 'dart:io' show Platform; import 'package:flutter/material.dart'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart' as http; import 'package:workmanager/workmanager.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'dart:developer' as dev; const _channelId = 'alert_channel'; const _channelName = 'Alerty'; const _channelDescription = 'Alerty z API (lodówka Ubibot)'; const String NOTIFS_ENABLED_KEY = 'NOTIFICATIONS_ENABLED'; final _notifs = FlutterLocalNotificationsPlugin(); Future _ensureNotificationChannel() async { final android = _notifs.resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>(); await android?.createNotificationChannel( const AndroidNotificationChannel( _channelId, _channelName, description: _channelDescription, importance: Importance.max, ), ); } NotificationDetails get _alertDetails => const NotificationDetails( android: AndroidNotificationDetails( _channelId, _channelName, channelDescription: _channelDescription, importance: Importance.max, priority: Priority.high, ), ); Future _safeGet(String url) async { try { return await http.get(Uri.parse(url)).timeout(const Duration(seconds: 30)); } catch (_) { try { return await http.get(Uri.parse(url)).timeout(const Duration(seconds: 30)); } catch (e) { return null; } } } @pragma('vm:entry-point') void callbackDispatcher() { WidgetsFlutterBinding.ensureInitialized(); Workmanager().executeTask((taskName, _inputData) async { const initAndroid = AndroidInitializationSettings('@mipmap/ic_launcher'); await _notifs.initialize( const InitializationSettings(android: initAndroid), ); await _ensureNotificationChannel(); final prefs = await SharedPreferences.getInstance(); final notificationsEnabled = prefs.getBool(NOTIFS_ENABLED_KEY) ?? true; if (!notificationsEnabled) return true; try { final url = prefs.getString('APIURL') ?? ''; final threshold = prefs.getDouble('TEMP_THRESHOLD') ?? 5.0; final response = await _safeGet(url); if (response == null) { throw Exception("Brak odpowiedzi z API (nawet po ponowieniu)."); } if (response.statusCode == 200) { final data = json.decode(response.body); final field8 = data['channel']?['last_values'] != null ? json.decode(data['channel']['last_values'])['field8'] : null; if (field8 != null && field8['value'] != null) { final double temp = field8['value'].toDouble(); if (temp > threshold) { await _notifs.show( DateTime.now().millisecondsSinceEpoch ~/ 1000, '⚠️ ALERT', 'Temperatura wzrosła powyżej progu: $temp°C > $threshold°C', _alertDetails, ); } } } else { throw Exception('Błąd API: ${response.statusCode}'); } } catch (e) { await _notifs.show( DateTime.now().millisecondsSinceEpoch ~/ 1000, 'Błąd API', e.toString(), _alertDetails, ); } return true; }); } Future main() async { dev.log('starting...', name: 'lodowka_ubibot'); WidgetsFlutterBinding.ensureInitialized(); final prefs = await SharedPreferences.getInstance(); await prefs.setString( 'APIURL', 'https://webapi.ubibot.com/channels/107563?api_key=58045f90a943499e83ad6e945c7719e8', ); const initAndroid = AndroidInitializationSettings('@mipmap/ic_launcher'); await _notifs.initialize(const InitializationSettings(android: initAndroid)); if (Platform.isAndroid) { final android = _notifs.resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>(); final granted = await android?.requestNotificationsPermission(); if (granted == null || !granted) { print('⚠️ Użytkownik odmówił POST_NOTIFICATIONS.'); } } await _ensureNotificationChannel(); await Workmanager().initialize(callbackDispatcher, isInDebugMode: false); await Workmanager().registerPeriodicTask( 'checkApiPeriodic', 'checkApi', frequency: const Duration(minutes: 15), existingWorkPolicy: ExistingWorkPolicy.keep, ); runApp(const MyApp()); } class MyApp extends StatefulWidget { const MyApp({super.key}); @override State createState() => _MyAppState(); } class _MyAppState extends State { final TextEditingController _controller = TextEditingController(); String _field8Value = ''; double _threshold = 5.0; bool _notificationsEnabled = true; @override void initState() { super.initState(); _loadSettingsAndFetch(); } Future _loadSettingsAndFetch() async { final prefs = await SharedPreferences.getInstance(); final url = prefs.getString('APIURL') ?? ''; _threshold = prefs.getDouble('TEMP_THRESHOLD') ?? 5.0; _notificationsEnabled = prefs.getBool(NOTIFS_ENABLED_KEY) ?? true; _controller.text = url; await _fetchField8(url); setState(() {}); } Future _saveApiUrl(String text) async { final prefs = await SharedPreferences.getInstance(); await prefs.setString("APIURL", text); } Future _saveThreshold() async { final prefs = await SharedPreferences.getInstance(); await prefs.setDouble("TEMP_THRESHOLD", _threshold); } Future _toggleNotifications(BuildContext context, bool enabled) async { final prefs = await SharedPreferences.getInstance(); await prefs.setBool(NOTIFS_ENABLED_KEY, enabled); setState(() => _notificationsEnabled = enabled); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Powiadomienia ${enabled ? "włączone" : "wyłączone"}'), duration: const Duration(seconds: 2), ), ); } Future _fetchField8([String? customUrl]) async { setState(() { _field8Value = 'Ładowanie…'; }); try { final prefs = await SharedPreferences.getInstance(); final url = (customUrl ?? _controller.text).trim(); if (url.isEmpty) { setState(() => _field8Value = 'Brak URL.'); return; } final response = await _safeGet(url); if (response == null) { setState(() => _field8Value = 'Brak odpowiedzi z API.'); return; } if (response.statusCode == 200) { final data = json.decode(response.body); final field8 = data['channel']?['last_values'] != null ? json.decode(data['channel']['last_values'])['field8'] : null; if (field8 != null && field8['value'] != null) { final double temp = field8['value'].toDouble(); setState(() => _field8Value = 'Temperatura w lodówce: $temp °C'); } else { setState(() => _field8Value = 'Brak danych field8.'); } } else { setState(() => _field8Value = 'Błąd: ${response.statusCode}'); } } catch (e) { setState(() => _field8Value = 'Wyjątek: $e'); } } Widget _buildBody(BuildContext context) { return SafeArea( child: SingleChildScrollView( child: Column( children: [ Padding( padding: const EdgeInsets.all(16.0), child: TextField( controller: _controller, decoration: const InputDecoration( border: OutlineInputBorder(), labelText: 'API URL', ), onChanged: _saveApiUrl, ), ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Column( children: [ const Text( 'Próg temperatury (°C)', style: TextStyle(fontSize: 16), ), const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ IconButton( onPressed: () { setState(() { _threshold = (_threshold - 0.1).clamp(-100.0, 100.0); }); _saveThreshold(); }, icon: const Icon(Icons.remove), ), Text( _threshold.toStringAsFixed(1), style: const TextStyle(fontSize: 20), ), IconButton( onPressed: () { setState(() { _threshold = (_threshold + 0.1).clamp(-100.0, 100.0); }); _saveThreshold(); }, icon: const Icon(Icons.add), ), ], ), ], ), ), SwitchListTile( title: const Text('Powiadomienia'), value: _notificationsEnabled, onChanged: (val) => _toggleNotifications(context, val), secondary: Icon( _notificationsEnabled ? Icons.notifications_active : Icons.notifications_off, ), ), ElevatedButton( onPressed: () => _fetchField8(), child: const Text('Sprawdź teraz'), ), Padding( padding: const EdgeInsets.all(16.0), child: Text( _field8Value, style: const TextStyle(fontSize: 20), ), ), ], ), ), ); } @override Widget build(BuildContext context) { return MaterialApp( title: 'API Alert App', debugShowCheckedModeBanner: false, theme: ThemeData(useMaterial3: true), home: Builder( builder: (context) => Scaffold( appBar: AppBar( title: const Text('UbiBot - powiadomienia'), centerTitle: true, backgroundColor: Colors.blue, ), body: _buildBody(context), ), ), ); } }