2024-03-12 20:41:04 +00:00
|
|
|
import 'dart:convert';
|
|
|
|
import 'package:anyhow/anyhow.dart';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
import 'package:flutter_dotenv/flutter_dotenv.dart';
|
|
|
|
import 'package:http/http.dart' as http;
|
|
|
|
import 'package:latlong2/latlong.dart';
|
|
|
|
import 'package:rust_core/option.dart';
|
|
|
|
import 'package:ws/ws.dart';
|
|
|
|
|
|
|
|
String _responseNiceErrorString(http.Response response) {
|
|
|
|
return 'Request failed: ${response.statusCode}: ${response.body.length > 500
|
|
|
|
? response.body.substring(0, 500)
|
|
|
|
: response.body}';
|
|
|
|
}
|
|
|
|
|
|
|
|
const API_PREFIX = '/api/0';
|
|
|
|
|
|
|
|
class OwntracksApi {
|
|
|
|
OwntracksApi({
|
|
|
|
required this.baseUrl,
|
|
|
|
required this.username,
|
|
|
|
required this.pass,
|
|
|
|
});
|
|
|
|
|
|
|
|
final String baseUrl;
|
|
|
|
final String username;
|
|
|
|
final String pass;
|
|
|
|
|
|
|
|
static Map<String, String> _createBasicAuthHeader(String username,
|
|
|
|
String password,
|
|
|
|
[Map<String, String>? additionalHeaders]) {
|
|
|
|
final credentials = base64Encode(utf8.encode('$username:$password'));
|
|
|
|
final headers = {
|
|
|
|
'Authorization': 'Basic $credentials',
|
|
|
|
};
|
|
|
|
|
|
|
|
// If there are additional headers, add them to the headers map
|
|
|
|
if (additionalHeaders != null) {
|
|
|
|
headers.addAll(additionalHeaders);
|
|
|
|
}
|
|
|
|
|
|
|
|
return headers;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Method to fetch points from a device within a specified date range
|
|
|
|
Future<Result<List<Point>>> fetchPointsForDevice({
|
|
|
|
required String user,
|
|
|
|
required String device,
|
|
|
|
required DateTime from,
|
|
|
|
DateTime? to,
|
|
|
|
}) async {
|
|
|
|
// Constructing the URL with query parameters
|
|
|
|
final queryParams = {
|
|
|
|
'user': user,
|
|
|
|
'device': device,
|
|
|
|
'from': from.toIso8601String(),
|
|
|
|
'to': (to ?? DateTime.now()).toIso8601String(),
|
|
|
|
'fields': 'lat,lon,isotst',
|
|
|
|
};
|
|
|
|
final uri = Uri.parse('$baseUrl$API_PREFIX/locations')
|
|
|
|
.replace(queryParameters: queryParams);
|
|
|
|
|
|
|
|
var auth_header = _createBasicAuthHeader(username, pass);
|
|
|
|
|
|
|
|
final response = await http.get(
|
|
|
|
uri,
|
|
|
|
headers: auth_header,
|
|
|
|
);
|
|
|
|
|
|
|
|
print(
|
|
|
|
'${response.statusCode}: ${response.body.length > 500 ? response.body
|
|
|
|
.substring(0, 500) : response.body}');
|
|
|
|
|
|
|
|
if (response.statusCode == 200) {
|
|
|
|
final jsonData =
|
|
|
|
(json.decode(response.body) as Map<String, dynamic>)['data'] as List;
|
|
|
|
return Ok(jsonData.map((point) => Point.fromJson(point)).toList());
|
|
|
|
} else {
|
|
|
|
return bail(
|
|
|
|
"couldn't get point data for device '$user:$device': ${_responseNiceErrorString(
|
|
|
|
response)}");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<Result<Map<String, List<String>>>> getDevices() async {
|
|
|
|
final uri = Uri.parse('$baseUrl$API_PREFIX/list');
|
|
|
|
print(uri);
|
|
|
|
|
|
|
|
final authHeader = _createBasicAuthHeader(username, pass);
|
|
|
|
|
|
|
|
final response = await http.get(uri, headers: authHeader);
|
|
|
|
|
|
|
|
if (response.statusCode != 200) {
|
|
|
|
return bail(
|
|
|
|
"couldn't get user list from server: ${_responseNiceErrorString(
|
|
|
|
response)}");
|
|
|
|
}
|
|
|
|
|
|
|
|
final users = List<String>.from(json.decode(response.body)['results']);
|
|
|
|
|
|
|
|
Map<String, List<String>> map = {};
|
|
|
|
|
|
|
|
for (String user in users) {
|
|
|
|
var response = await http.get(
|
|
|
|
uri.replace(queryParameters: {'user': user}),
|
|
|
|
headers: authHeader);
|
|
|
|
|
|
|
|
if (response.statusCode != 200) {
|
|
|
|
return bail(
|
|
|
|
"couldn't get devices list for user '$user': ${_responseNiceErrorString(
|
|
|
|
response)}");
|
|
|
|
}
|
|
|
|
|
|
|
|
map[user] = List<String>.from(jsonDecode(response.body)['results']);
|
|
|
|
}
|
|
|
|
|
|
|
|
return Ok(map);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Method to create and return a WebSocket connection
|
2024-03-14 19:50:30 +00:00
|
|
|
Future<Result<WebSocketClient>> createWebSocketConnection({
|
2024-03-12 20:41:04 +00:00
|
|
|
required String wsPath,
|
|
|
|
required void Function(Object message) onMessage,
|
|
|
|
required void Function(WebSocketClientState stateChange) onStateChange,
|
|
|
|
Option<(String, String)> onlyDeviceId = None,
|
|
|
|
}) async {
|
|
|
|
const retryInterval = (
|
|
|
|
min: Duration(milliseconds: 500),
|
|
|
|
max: Duration(seconds: 15),
|
|
|
|
);
|
|
|
|
|
|
|
|
Map<String, String> headers = {};
|
|
|
|
|
|
|
|
if (onlyDeviceId case Some(:final v)){
|
|
|
|
headers.putIfAbsent('X-Limit-User', () => v.$1);
|
|
|
|
headers.putIfAbsent('X-Limit-Device', () => v.$2);
|
|
|
|
}
|
|
|
|
|
|
|
|
final client = WebSocketClient(kIsWeb
|
|
|
|
? WebSocketOptions.common(connectionRetryInterval: retryInterval)
|
|
|
|
: WebSocketOptions.vm(
|
|
|
|
connectionRetryInterval: retryInterval,
|
|
|
|
headers: _createBasicAuthHeader(username, pass)..addAll(headers)));
|
|
|
|
|
|
|
|
// Listen to messages
|
|
|
|
client.stream.listen(onMessage);
|
|
|
|
|
|
|
|
// Listen to state changes
|
|
|
|
client.stateChanges.listen(onStateChange);
|
|
|
|
|
|
|
|
// Connect to the WebSocket server
|
2024-03-14 19:50:30 +00:00
|
|
|
try {
|
|
|
|
await client.connect("${baseUrl.replaceFirst('http', 'ws')}/ws/$wsPath");
|
|
|
|
} catch (e) {
|
|
|
|
await client.disconnect();
|
|
|
|
return bail("WebSocket connection to path $wsPath was unsuccessful: $e");
|
|
|
|
}
|
2024-03-12 20:41:04 +00:00
|
|
|
|
|
|
|
// Return the connected client
|
2024-03-14 19:50:30 +00:00
|
|
|
return Ok(client);
|
2024-03-12 20:41:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class Point {
|
|
|
|
final double lat;
|
|
|
|
final double lon;
|
|
|
|
final DateTime timestamp;
|
|
|
|
|
|
|
|
Point({required this.lat, required this.lon, required this.timestamp});
|
|
|
|
|
2024-03-14 19:50:30 +00:00
|
|
|
@override
|
|
|
|
String toString() {
|
|
|
|
return 'Point{lat: $lat, lon: $lon, timestamp: $timestamp}';
|
|
|
|
}
|
|
|
|
|
2024-03-12 20:41:04 +00:00
|
|
|
factory Point.fromJson(Map<String, dynamic> json) {
|
|
|
|
return Point(
|
|
|
|
lat: json['lat'],
|
|
|
|
lon: json['lon'],
|
|
|
|
timestamp: DateTime.parse(json['isotst']),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
LatLng get asLatLng => LatLng(lat, lon);
|
|
|
|
}
|
|
|
|
|
|
|
|
class Device {
|
|
|
|
final String id;
|
|
|
|
final String name;
|
|
|
|
|
|
|
|
Device({required this.id, required this.name});
|
|
|
|
|
|
|
|
factory Device.fromJson(Map<String, dynamic> json) {
|
|
|
|
return Device(
|
|
|
|
id: json['id'],
|
|
|
|
name: json['name'],
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
Future<List<Point>> fetchPointsForDevice(String deviceId) async {
|
|
|
|
final response = await http.get(Uri.parse('$_baseUrl/devices/$deviceId/points'),
|
|
|
|
headers: <String, String> {
|
|
|
|
'authorization': base64Encode(utf8.encode('$username:$pass'))
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
print('${response.statusCode}: ${response.body}');
|
|
|
|
|
|
|
|
if (response.statusCode == 200) {
|
|
|
|
final jsonData = json.decode(response.body);
|
|
|
|
return (jsonData as List).map((point) => Point.fromJson(point)).toList();
|
|
|
|
} else {
|
|
|
|
throw Exception('Failed to load points');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<List<Device>> listAllDevices() async {
|
|
|
|
final response = await http.get(Uri.parse('$_baseUrl/devices'));
|
|
|
|
if (response.statusCode == 200) {
|
|
|
|
final jsonData = json.decode(response.body);
|
|
|
|
return (jsonData as List).map((device) => Device.fromJson(device)).toList();
|
|
|
|
} else {
|
|
|
|
throw Exception('Failed to load devices');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add more API calls as needed
|
|
|
|
}
|
|
|
|
|
|
|
|
*/
|