344 lines
10 KiB
Dart
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),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|