owntracks-viewer/lib/owntracks_api.dart

233 lines
6.4 KiB
Dart
Raw Normal View History

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
}
*/