lodowka/lib/main.dart
2025-07-21 13:42:43 +02:00

344 lines
10 KiB
Dart

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<void> _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<http.Response?> _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<void> 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<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final TextEditingController _controller = TextEditingController();
String _field8Value = '';
double _threshold = 5.0;
bool _notificationsEnabled = true;
@override
void initState() {
super.initState();
_loadSettingsAndFetch();
}
Future<void> _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<void> _saveApiUrl(String text) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString("APIURL", text);
}
Future<void> _saveThreshold() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setDouble("TEMP_THRESHOLD", _threshold);
}
Future<void> _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<void> _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),
),
),
);
}
}