From 5baca9f3beb37fd3674fed1947b788f077c42478 Mon Sep 17 00:00:00 2001 From: ske087 Date: Mon, 2 Jun 2025 15:45:17 +0300 Subject: [PATCH] initial commit --- .gitignore | 2 + collors info.txt | 9 + main.py | 358 +++++ openapi.yaml | 2381 +++++++++++++++++++++++++++++++++ reqirements.txt | 2 + resources/credentials.enc | 1 + resources/key.key | 1 + resources/server_settings.enc | 1 + resources/track.png | Bin 0 -> 23869 bytes test_traccar_trip.py | 80 ++ traccar.kv | 347 +++++ utils.py | 220 +++ 12 files changed, 3402 insertions(+) create mode 100644 .gitignore create mode 100644 collors info.txt create mode 100644 main.py create mode 100644 openapi.yaml create mode 100644 reqirements.txt create mode 100644 resources/credentials.enc create mode 100644 resources/key.key create mode 100644 resources/server_settings.enc create mode 100644 resources/track.png create mode 100644 test_traccar_trip.py create mode 100644 traccar.kv create mode 100644 utils.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..65ad9b9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +# Ignore the virtual environment folder +track/ \ No newline at end of file diff --git a/collors info.txt b/collors info.txt new file mode 100644 index 0000000..1863272 --- /dev/null +++ b/collors info.txt @@ -0,0 +1,9 @@ +To update the colors to the specified values, we will convert the hex color codes to RGBA format (values between 0 and 1) and update the `server_box_color` property in the `HomeScreen` class. + +Here are the RGBA equivalents of the provided hex colors: + +- **Yellow (#FB8D14)**: `(0.984, 0.553, 0.078, 1)` +- **Red (#E8083E)**: `(0.909, 0.031, 0.243, 1)` +- **Green (#02864A)**: `(0.008, 0.525, 0.290, 1)` +The RGBA equivalent of `#573CFA` is `(0.341, 0.235, 0.980, 1)`. + diff --git a/main.py b/main.py new file mode 100644 index 0000000..ee330bf --- /dev/null +++ b/main.py @@ -0,0 +1,358 @@ +import kivy +from kivy.app import App +from kivy.uix.screenmanager import ScreenManager, Screen +import requests +import os +import json +from cryptography.fernet import Fernet +from kivy.clock import Clock +from kivy.properties import StringProperty, ListProperty +from utils import get_devices_from_server +from utils import check_server_settings # Import the refactored function +from utils import test_connection # Import the refactored function +from datetime import date +from kivy.uix.popup import Popup +from kivy.uix.gridlayout import GridLayout +from kivy.uix.button import Button +from kivy.uix.label import Label +kivy.require("2.0.0") # Ensure the correct Kivy version is used + +# Paths +RESOURCES_FOLDER = "resources" +CREDENTIALS_FILE = os.path.join(RESOURCES_FOLDER, "credentials.enc") +KEY_FILE = os.path.join(RESOURCES_FOLDER, "key.key") + + +# Utility functions for encryption +def generate_key(): + """Generate and save a key for encryption.""" + if not os.path.exists(KEY_FILE): + key = Fernet.generate_key() + with open(KEY_FILE, "wb") as key_file: + key_file.write(key) + + +def load_key(): + """Load the encryption key.""" + with open(KEY_FILE, "rb") as key_file: + return key_file.read() + + +def encrypt_data(data): + """Encrypt data using the encryption key.""" + key = load_key() + fernet = Fernet(key) + return fernet.encrypt(data.encode()) + + +def decrypt_data(data): + """Decrypt data using the encryption key.""" + key = load_key() + fernet = Fernet(key) + return fernet.decrypt(data).decode() + + +# Login Screen +class LoginScreen(Screen): + def login(self): + """Handle login and save credentials.""" + username = self.ids.username_input.text.strip() + password = self.ids.password_input.text.strip() + + if not username or not password: + self.manager.get_screen("home").ids.result_label.text = "Please fill in all fields." + return + + # Encrypt and save credentials + credentials = {"username": username, "password": password} + encrypted_data = encrypt_data(json.dumps(credentials)) + with open(CREDENTIALS_FILE, "wb") as file: + file.write(encrypted_data) + + # Navigate to the home screen + self.manager.current = "home" + + +# Settings Screen (renamed from MainScreen) +class SettingsScreen(Screen): + server_response = "Waiting to test connection..." + + def on_pre_enter(self): + """Load existing settings into the input fields when the screen is entered.""" + settings_file = os.path.join(RESOURCES_FOLDER, "server_settings.enc") + if os.path.exists(settings_file): + try: + with open(settings_file, "rb") as file: + encrypted_data = file.read() + decrypted_data = decrypt_data(encrypted_data) + settings_data = json.loads(decrypted_data) + + # Populate the input fields with the existing settings + self.ids.server_url_input.text = settings_data.get("server_url", "") + self.ids.username_input.text = settings_data.get("username", "") + self.ids.password_input.text = settings_data.get("password", "") + self.ids.token_input.text = settings_data.get("token", "") # Populate the token field + except Exception as e: + self.ids.result_label.text = f"Failed to load settings: {str(e)}" + else: + # Clear the input fields if no settings exist + self.ids.server_url_input.text = "" + self.ids.username_input.text = "" + self.ids.password_input.text = "" + self.ids.token_input.text = "" + + def test_connection(self): + """Test the connection with the Traccar server.""" + # Get input values from the screen + server_url = self.ids.server_url_input.text.strip() + username = self.ids.username_input.text.strip() + password = self.ids.password_input.text.strip() + token = self.ids.token_input.text.strip() # Get the token input + + # Call the refactored function + result = test_connection(server_url, username, password, token) + + # Update the UI based on the result + self.server_response = result["message"] + self.ids.result_label.text = self.server_response + + def save_settings(self): + """Save the server settings to an encrypted file and navigate to the HomeScreen.""" + server_url = self.ids.server_url_input.text.strip() + username = self.ids.username_input.text.strip() + password = self.ids.password_input.text.strip() + token = self.ids.token_input.text.strip() # Get the token input + + if not server_url or not username or not password or not token: + self.ids.result_label.text = "Please fill in all fields." + return + + settings_data = { + "server_url": server_url, + "username": username, + "password": password, + "token": token, # Include the token in the data + } + encrypted_data = encrypt_data(json.dumps(settings_data)) + + try: + # Save the encrypted data to the server_settings.enc file + settings_file = os.path.join(RESOURCES_FOLDER, "server_settings.enc") + with open(settings_file, "wb") as file: + file.write(encrypted_data) + + self.ids.result_label.text = "Settings saved successfully!" + # Navigate back to the HomeScreen + self.manager.current = "home" + except Exception as e: + self.ids.result_label.text = f"Failed to save settings: {str(e)}" + + +# Home Screen +class HomeScreen(Screen): + server_info_text = StringProperty("LOADING DATA...") # Default text for the label + server_box_color = ListProperty([0.984, 0.553, 0.078, 1]) # Default yellow color (#FB8D14) + device_mapping = {} # Store the mapping of device names to IDs + + def on_pre_enter(self): + """Start the flow for checking server information and connection.""" + self.server_box_color = [0.984, 0.553, 0.078, 1] # Yellow color (#FB8D14) + self.server_info_text = "LOADING DATA..." + Clock.schedule_once(self.check_server_settings, 1) # Wait 1 second before checking settings + + def check_server_settings(self, dt): + """Check server settings and update the UI.""" + settings = check_server_settings() # Call the refactored function + if settings: + server_url = settings["server_url"] + username = settings["username"] + password = settings["password"] + token = settings["token"] + + # Update the label to indicate checking connection + self.server_info_text = f"CHECKING server: {server_url}" + self.server_box_color = [0.984, 0.553, 0.078, 1] # Keep yellow color (#FB8D14) + self.ids.devices_spinner.text = "Loading devices..." # Show loading text + + # Test the connection after loading settings + Clock.schedule_once(lambda dt: self.test_connection(server_url, username, password, token), 1) + else: + self.server_info_text = "Go to settings and set Traccar Server" + self.server_box_color = [0.909, 0.031, 0.243, 1] # Red color (#E8083E) + self.ids.devices_spinner.text = "No devices available" + + def test_connection(self, server_url, username, password, token): + """Test the connection with the Traccar server.""" + try: + # Test the connection with the server + headers = {"Authorization": f"Bearer {token}"} if token else None + auth = None if token else (username, password) + + print(f"Testing connection to {server_url}") + response = requests.get(f"{server_url}/api/server", headers=headers, auth=auth, timeout=10) + + if response.status_code == 200: + # Update the label and box color for a successful connection + self.server_info_text = f"Connected to {server_url}" + self.server_box_color = [0.008, 0.525, 0.290, 1] # Green color (#02864A) + print(f"Connection successful: {self.server_info_text}") + + # Fetch devices and populate the dropdown + devices = get_devices_from_server() + if devices: + self.device_mapping = devices # Store the mapping of device names to IDs + device_names = list(devices.keys()) # Get the list of device names + self.ids.devices_spinner.values = device_names # Populate the dropdown + self.ids.devices_spinner.text = "Select a device" # Default text after loading + else: + self.ids.devices_spinner.text = "No devices found" + self.ids.devices_spinner.values = [] + else: + # Update the label and box color for a failed connection + self.server_info_text = f"Error: {response.status_code} - {response.reason}" + self.server_box_color = [0.909, 0.031, 0.243, 1] # Red color (#E8083E) + print(f"Connection failed: {self.server_info_text}") + except requests.exceptions.RequestException as e: + # Update the label and box color for a connection error + self.server_info_text = f"Connection failed: {str(e)}" + self.server_box_color = [0.909, 0.031, 0.243, 1] # Red color (#E8083E) + print(f"Connection error: {str(e)}") + + def on_device_selected(self, device_name): + """Change the background color of the Spinner when a device is selected.""" + if device_name != "Loading devices..." and device_name != "No devices found": + self.ids.devices_spinner.background_color = (0.008, 0.525, 0.290, 1) # Green color (#02864A) + print(f"Device selected: {device_name}") + else: + self.ids.devices_spinner.background_color = (1, 1, 1, 1) # Reset to white if no valid device is selected + + def open_date_picker(self): + """Open a popup to select a date.""" + today = date.today() + selected_date = [None] # Use a mutable object to store the selected date + + def on_date_selected(instance): + selected_date[0] = instance.text + # Update the button text with the selected date + self.ids.date_picker_button.text = f"Date: {today.year}-{today.month:02d}-{int(selected_date[0]):02d}" + # Change the background color of the button to green + self.ids.date_picker_button.background_color = (0.008, 0.525, 0.290, 1) # Green color (#02864A) + print(f"Date selected: {self.ids.date_picker_button.text}") + popup.dismiss() + + # Create a popup with a grid layout for the days + layout = GridLayout(cols=7, spacing=5, padding=10) + for day in range(1, 32): # Assuming a maximum of 31 days in a month + try: + current_date = date(today.year, today.month, day) + button = Button(text=str(day), size_hint=(None, None), size=(40, 40)) + button.bind(on_press=on_date_selected) + layout.add_widget(button) + except ValueError: + # Skip invalid dates (e.g., February 30) + pass + + popup = Popup(title="Select a Date", content=layout, size_hint=(0.8, 0.8)) + popup.open() + + + + def get_trip_server_data(self): + """Handle the Get trip server data button press.""" + selected_device = self.ids.devices_spinner.text + selected_date = self.ids.date_picker_button.text + + if selected_device == "Loading devices..." or selected_device == "No devices found": + print("No valid device selected.") + self.ids.result_label.text = "Please select a valid device." + return + + if selected_date == "Select Date": + print("No valid date selected.") + self.ids.result_label.text = "Please select a valid date." + return + + # Simulate fetching trip data from the server + print(f"Fetching trip data for device: {selected_device} on date: {selected_date}") + self.ids.result_label.text = f"Fetching trip data for {selected_device} on {selected_date}..." + + +class RegisterScreen(Screen): + def create_user(self): + """Handle user creation.""" + username = self.ids.set_username_input.text.strip() + password = self.ids.set_password_input.text.strip() + confirm_password = self.ids.confirm_password_input.text.strip() + email = self.ids.set_email_input.text.strip() + + if not username or not password or not confirm_password or not email: + self.ids.result_label.text = "Please fill in all fields." + return + + if password != confirm_password: + self.ids.result_label.text = "Passwords do not match." + return + + # Check if the username or email already exists + if self.user_exists(username, email): + self.ids.result_label.text = "User or email already exists." + return + + # Save user data (encrypted) + user_data = { + "username": username, + "password": password, + "email": email, + } + encrypted_data = encrypt_data(json.dumps(user_data)) + + try: + with open(CREDENTIALS_FILE, "ab") as file: # Append encrypted data + file.write(encrypted_data + b"\n") # Add a newline for separation + except Exception as e: + self.ids.result_label.text = f"Failed to save user: {str(e)}" + return + + self.ids.result_label.text = "User created successfully!" + # Navigate back to the login screen + self.manager.current = "login" + + def user_exists(self, username, email): + """Check if a username or email already exists in the credentials.enc file.""" + try: + with open(CREDENTIALS_FILE, "rb") as file: + for line in file: + decrypted_data = decrypt_data(line.strip()) + user = json.loads(decrypted_data) + if user["username"] == username or user["email"] == email: + return True + except FileNotFoundError: + pass + except Exception as e: + self.ids.result_label.text = f"Error checking user: {str(e)}" + return False +# Main App +class TraccarApp(App): + def build(self): + # Ensure resources folder exists + if not os.path.exists(RESOURCES_FOLDER): + os.makedirs(RESOURCES_FOLDER) + + # Generate encryption key if it doesn't exist + generate_key() + + # Screen manager + sm = ScreenManager() + sm.add_widget(LoginScreen(name="login")) + sm.add_widget(HomeScreen(name="home")) # Add the HomeScreen + sm.add_widget(SettingsScreen(name="settings")) # Add the renamed SettingsScreen + sm.add_widget(RegisterScreen(name="register")) # Add the RegisterScreen + + # Debugging: Print all screen names + print("Screens added to ScreenManager:", [screen.name for screen in sm.screens]) + + return sm + + +if __name__ == "__main__": + TraccarApp().run() \ No newline at end of file diff --git a/openapi.yaml b/openapi.yaml new file mode 100644 index 0000000..86ff751 --- /dev/null +++ b/openapi.yaml @@ -0,0 +1,2381 @@ +openapi: 3.1.0 +info: + title: Traccar + description: Traccar GPS tracking server API documentation. To use the API you need to have a server instance. For testing purposes you can use one of free [demo servers](https://www.traccar.org/demo-server/). For production use you can install your own server or get a [subscription service](https://www.traccar.org/product/tracking-server/). + contact: + name: Traccar Support + url: https://www.traccar.org/ + email: support@traccar.org + license: + name: Apache 2.0 + url: https://www.apache.org/licenses/LICENSE-2.0.html + version: '6.6' +servers: + - url: https://demo.traccar.org/api + description: Demo Server 1 + - url: https://demo2.traccar.org/api + description: Demo Server 2 + - url: https://demo3.traccar.org/api + description: Demo Server 3 + - url: https://demo4.traccar.org/api + description: Demo Server 4 + - url: https://server.traccar.org/api + description: Subscription Server + - url: http://{host}:{port}/api + description: Other Server + variables: + host: + default: localhost + port: + enum: + - '8082' + - '80' + default: '8082' +security: + - BasicAuth: [] + - ApiKey: [] +tags: + - name: Server + description: Server information + - name: Session + description: User session management + - name: Devices + description: Device management + - name: Groups + description: Group management + - name: Users + description: User management + - name: Permissions + description: User permissions and other object linking + - name: Positions + description: Retrieving raw location information + - name: Events + description: Retrieving event information + - name: Reports + description: Reports generation + - name: Notifications + description: User notifications management + - name: Geofences + description: Geofence management + - name: Commands + description: Sending commands to devices and stored command management + - name: Attributes + description: Computed attributes management + - name: Drivers + description: Drivers management + - name: Maintenance + description: Maintenance management + - name: Calendars + description: Calendar management + - name: Statistics + description: Retrieving server statistics +paths: + /commands: + get: + summary: Fetch a list of Saved Commands + tags: + - Commands + description: Without params, it returns a list of Saved Commands the user has access to + parameters: + - name: all + in: query + description: Can only be used by admins or managers to fetch all entities + schema: + type: boolean + - name: userId + in: query + description: Standard users can use this only with their own _userId_ + schema: + type: integer + - name: deviceId + in: query + description: Standard users can use this only with _deviceId_s, they have access to + schema: + type: integer + - name: groupId + in: query + description: >- + Standard users can use this only with _groupId_s, they have access + to + schema: + type: integer + - name: refresh + in: query + schema: + type: boolean + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Command' + post: + summary: Create a Saved Command + tags: + - Commands + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Command' + required: true + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Command' + x-codegen-request-body-name: body + /commands/{id}: + put: + summary: Update a Saved Command + tags: + - Commands + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Command' + required: true + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Command' + x-codegen-request-body-name: body + delete: + summary: Delete a Saved Command + tags: + - Commands + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '204': + description: No Content + content: {} + /commands/send: + get: + summary: Fetch a list of Saved Commands supported by Device at the moment + description: >- + Return a list of saved commands linked to Device and its groups, + filtered by current Device protocol support + tags: + - Commands + parameters: + - name: deviceId + in: query + description: >- + Standard users can use this only with _deviceId_s, they have access + to + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Command' + '400': + description: Could happen when the user doesn't have permission for the device + content: {} + post: + summary: Dispatch commands to device + description: Dispatch a new command or Saved Command if _body.id_ set + tags: + - Commands + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Command' + required: true + responses: + '200': + description: Command sent + content: + application/json: + schema: + $ref: '#/components/schemas/Command' + '202': + description: Command queued + content: + application/json: + schema: + $ref: '#/components/schemas/Command' + '400': + description: >- + Could happen when the user doesn't have permission or an incorrect + command _type_ for the device + content: {} + x-codegen-request-body-name: body + /commands/types: + get: + summary: >- + Fetch a list of available Commands for the Device or all possible + Commands if Device ommited + tags: + - Commands + parameters: + - name: deviceId + in: query + description: >- + Internal device identifier. Only works if device has already + reported some locations + schema: + type: integer + - name: protocol + in: query + description: Protocol name. Can be used instead of device id + schema: + type: string + - name: textChannel + in: query + description: >- + When `true` return SMS commands. If not specified or `false` return + data commands + schema: + type: boolean + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/CommandType' + '400': + description: >- + Could happen when trying to fetch from a device the user does not + have permission + content: {} + /devices: + get: + summary: Fetch a list of Devices + description: Without any params, returns a list of the user's devices + tags: + - Devices + parameters: + - name: all + in: query + description: Can only be used by admins or managers to fetch all entities + schema: + type: boolean + - name: userId + in: query + description: Standard users can use this only with their own _userId_ + schema: + type: integer + - name: id + in: query + description: >- + To fetch one or more devices. Multiple params can be passed like + `id=31&id=42` + schema: + type: integer + - name: uniqueId + in: query + description: >- + To fetch one or more devices. Multiple params can be passed like + `uniqueId=333331&uniqieId=44442` + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Device' + '400': + description: No permission + content: {} + post: + summary: Create a Device + tags: + - Devices + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Device' + required: true + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Device' + x-codegen-request-body-name: body + /devices/{id}: + put: + summary: Update a Device + tags: + - Devices + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Device' + required: true + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Device' + x-codegen-request-body-name: body + delete: + summary: Delete a Device + tags: + - Devices + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '204': + description: No Content + content: {} + /devices/{id}/accumulators: + put: + summary: Update total distance and hours of the Device + tags: + - Devices + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DeviceAccumulators' + required: true + responses: + '204': + description: No Content + content: {} + x-codegen-request-body-name: body + /groups: + get: + summary: Fetch a list of Groups + description: Without any params, returns a list of the Groups the user belongs to + tags: + - Groups + parameters: + - name: all + in: query + description: Can only be used by admins or managers to fetch all entities + schema: + type: boolean + - name: userId + in: query + description: Standard users can use this only with their own _userId_ + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Group' + post: + summary: Create a Group + tags: + - Groups + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Group' + required: true + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Group' + '400': + description: No permission + content: {} + x-codegen-request-body-name: body + /groups/{id}: + put: + summary: Update a Group + tags: + - Groups + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Group' + required: true + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Group' + x-codegen-request-body-name: body + delete: + summary: Delete a Group + tags: + - Groups + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '204': + description: No Content + content: {} + /permissions: + post: + summary: Link an Object to another Object + tags: + - Permissions + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Permission' + required: true + responses: + '204': + description: No Content + content: {} + '400': + description: No permission + content: {} + x-codegen-request-body-name: body + delete: + summary: Unlink an Object from another Object + tags: + - Permissions + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Permission' + required: true + responses: + '204': + description: No Content + content: {} + x-codegen-request-body-name: body + /positions: + get: + summary: Fetches a list of Positions + description: >- + We strongly recommend using [Traccar WebSocket + API](https://www.traccar.org/traccar-api/) instead of periodically + polling positions endpoint. Without any params, it returns a list of + last known positions for all the user's Devices. _from_ and _to_ fields + are not required with _id_. + tags: + - Positions + parameters: + - name: deviceId + in: query + description: >- + _deviceId_ is optional, but requires the _from_ and _to_ parameters + when used + schema: + type: integer + - name: from + in: query + description: in IS0 8601 format. eg. `1963-11-22T18:30:00Z` + schema: + type: string + format: date-time + - name: to + in: query + description: in IS0 8601 format. eg. `1963-11-22T18:30:00Z` + schema: + type: string + format: date-time + - name: id + in: query + description: >- + To fetch one or more positions. Multiple params can be passed like + `id=31&id=42` + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Position' + text/csv: + schema: + type: array + items: + $ref: '#/components/schemas/Position' + application/gpx+xml: + schema: + type: array + items: + $ref: '#/components/schemas/Position' + delete: + summary: Deletes all the Positions of a device in the time span specified + description: '' + tags: + - Positions + parameters: + - name: deviceId + in: query + description: '' + schema: + type: integer + required: true + - name: from + in: query + description: in IS0 8601 format. eg. `1963-11-22T18:30:00Z` + schema: + type: string + format: date-time + required: true + - name: to + in: query + description: in IS0 8601 format. eg. `1963-11-22T18:30:00Z` + schema: + type: string + format: date-time + required: true + responses: + '204': + description: No Content + content: {} + '400': + description: Bad Request + content: {} + /positions/{id}: + delete: + summary: Delete a Position + tags: + - Positions + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '204': + description: No Content + content: {} + '404': + description: Not Found + content: {} + /server: + get: + summary: Fetch Server information + tags: + - Server + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Server' + put: + summary: Update Server information + tags: + - Server + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Server' + required: true + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Server' + x-codegen-request-body-name: body + /session: + get: + summary: Fetch Session information + tags: + - Session + parameters: + - name: token + in: query + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '404': + description: Not Found + content: {} + post: + summary: Create a new Session + tags: + - Session + requestBody: + content: + application/x-www-form-urlencoded: + schema: + required: + - email + - password + properties: + email: + type: string + password: + type: string + format: password + required: true + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/User' + '401': + description: Unauthorized + content: {} + delete: + summary: Close the Session + tags: + - Session + responses: + '204': + description: No Content + content: {} + /session/openid/auth: + get: + summary: Fetch Session information + tags: + - Session + responses: + '303': + description: Redirect to OpenID Connect identity provider + content: {} + /session/openid/callback: + get: + summary: OpenID Callback + tags: + - Session + responses: + '303': + description: Successful authentication, redirect to homepage + content: {} + /users: + get: + summary: Fetch a list of Users + tags: + - Users + parameters: + - name: userId + in: query + description: Can only be used by admin or manager users + schema: + type: string + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/User' + '400': + description: No Permission + content: {} + post: + summary: Create a User + tags: + - Users + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + required: true + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/User' + x-codegen-request-body-name: body + /users/{id}: + put: + summary: Update a User + tags: + - Users + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + required: true + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/User' + x-codegen-request-body-name: body + delete: + summary: Delete a User + tags: + - Users + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '204': + description: No Content + content: {} + /notifications: + get: + summary: Fetch a list of Notifications + description: >- + Without params, it returns a list of Notifications the user has access + to + tags: + - Notifications + parameters: + - name: all + in: query + description: Can only be used by admins or managers to fetch all entities + schema: + type: boolean + - name: userId + in: query + description: Standard users can use this only with their own _userId_ + schema: + type: integer + - name: deviceId + in: query + description: >- + Standard users can use this only with _deviceId_s, they have access + to + schema: + type: integer + - name: groupId + in: query + description: >- + Standard users can use this only with _groupId_s, they have access + to + schema: + type: integer + - name: refresh + in: query + schema: + type: boolean + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Notification' + post: + summary: Create a Notification + tags: + - Notifications + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Notification' + required: true + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Notification' + x-codegen-request-body-name: body + /notifications/{id}: + put: + summary: Update a Notification + tags: + - Notifications + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Notification' + required: true + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Notification' + x-codegen-request-body-name: body + delete: + summary: Delete a Notification + tags: + - Notifications + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '204': + description: No Content + content: {} + /notifications/types: + get: + summary: Fetch a list of available Notification types + tags: + - Notifications + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/NotificationType' + /notifications/test: + post: + summary: Send test notification to current user via Email and SMS + tags: + - Notifications + responses: + '204': + description: Successful sending + content: {} + '400': + description: Could happen if sending has failed + content: {} + /geofences: + get: + summary: Fetch a list of Geofences + description: Without params, it returns a list of Geofences the user has access to + tags: + - Geofences + parameters: + - name: all + in: query + description: Can only be used by admins or managers to fetch all entities + schema: + type: boolean + - name: userId + in: query + description: Standard users can use this only with their own _userId_ + schema: + type: integer + - name: deviceId + in: query + description: >- + Standard users can use this only with _deviceId_s, they have access + to + schema: + type: integer + - name: groupId + in: query + description: >- + Standard users can use this only with _groupId_s, they have access + to + schema: + type: integer + - name: refresh + in: query + schema: + type: boolean + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Geofence' + post: + summary: Create a Geofence + tags: + - Geofences + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Geofence' + required: true + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Geofence' + x-codegen-request-body-name: body + /geofences/{id}: + put: + summary: Update a Geofence + tags: + - Geofences + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Geofence' + required: true + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Geofence' + x-codegen-request-body-name: body + delete: + summary: Delete a Geofence + tags: + - Geofences + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '204': + description: No Content + content: {} + /events/{id}: + get: + tags: + - Events + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Event' + /reports/route: + get: + summary: >- + Fetch a list of Positions within the time period for the Devices or + Groups + description: At least one _deviceId_ or one _groupId_ must be passed + tags: + - Reports + parameters: + - name: deviceId + in: query + style: form + explode: true + schema: + type: array + items: + type: integer + - name: groupId + in: query + style: form + explode: true + schema: + type: array + items: + type: integer + - name: from + in: query + description: in IS0 8601 format. eg. `1963-11-22T18:30:00Z` + required: true + schema: + type: string + format: date-time + - name: to + in: query + description: in IS0 8601 format. eg. `1963-11-22T18:30:00Z` + required: true + schema: + type: string + format: date-time + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Position' + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet: + schema: + type: array + items: + $ref: '#/components/schemas/Position' + /reports/events: + get: + summary: Fetch a list of Events within the time period for the Devices or Groups + description: At least one _deviceId_ or one _groupId_ must be passed + tags: + - Reports + parameters: + - name: deviceId + in: query + style: form + explode: true + schema: + type: array + items: + type: integer + - name: groupId + in: query + style: form + explode: true + schema: + type: array + items: + type: integer + - name: type + in: query + description: '% can be used to return events of all types' + style: form + explode: false + schema: + type: array + items: + type: string + - name: from + in: query + description: in IS0 8601 format. eg. `1963-11-22T18:30:00Z` + required: true + schema: + type: string + format: date-time + - name: to + in: query + description: in IS0 8601 format. eg. `1963-11-22T18:30:00Z` + required: true + schema: + type: string + format: date-time + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Event' + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet: + schema: + type: array + items: + $ref: '#/components/schemas/Event' + /reports/summary: + get: + summary: >- + Fetch a list of ReportSummary within the time period for the Devices or + Groups + description: At least one _deviceId_ or one _groupId_ must be passed + tags: + - Reports + parameters: + - name: deviceId + in: query + style: form + explode: true + schema: + type: array + items: + type: integer + - name: groupId + in: query + style: form + explode: true + schema: + type: array + items: + type: integer + - name: from + in: query + description: in IS0 8601 format. eg. `1963-11-22T18:30:00Z` + required: true + schema: + type: string + format: date-time + - name: to + in: query + description: in IS0 8601 format. eg. `1963-11-22T18:30:00Z` + required: true + schema: + type: string + format: date-time + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ReportSummary' + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet: + schema: + type: array + items: + $ref: '#/components/schemas/ReportSummary' + /reports/trips: + get: + summary: >- + Fetch a list of ReportTrips within the time period for the Devices or + Groups + description: At least one _deviceId_ or one _groupId_ must be passed + tags: + - Reports + parameters: + - name: deviceId + in: query + style: form + explode: true + schema: + type: array + items: + type: integer + - name: groupId + in: query + style: form + explode: true + schema: + type: array + items: + type: integer + - name: from + in: query + description: in IS0 8601 format. eg. `1963-11-22T18:30:00Z` + required: true + schema: + type: string + format: date-time + - name: to + in: query + description: in IS0 8601 format. eg. `1963-11-22T18:30:00Z` + required: true + schema: + type: string + format: date-time + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ReportTrips' + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet: + schema: + type: array + items: + $ref: '#/components/schemas/ReportTrips' + /reports/stops: + get: + summary: >- + Fetch a list of ReportStops within the time period for the Devices or + Groups + description: At least one _deviceId_ or one _groupId_ must be passed + tags: + - Reports + parameters: + - name: deviceId + in: query + style: form + explode: true + schema: + type: array + items: + type: integer + - name: groupId + in: query + style: form + explode: true + schema: + type: array + items: + type: integer + - name: from + in: query + description: in IS0 8601 format. eg. `1963-11-22T18:30:00Z` + required: true + schema: + type: string + format: date-time + - name: to + in: query + description: in IS0 8601 format. eg. `1963-11-22T18:30:00Z` + required: true + schema: + type: string + format: date-time + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/ReportStops' + application/vnd.openxmlformats-officedocument.spreadsheetml.sheet: + schema: + type: array + items: + $ref: '#/components/schemas/ReportStops' + /statistics: + get: + summary: Fetch server Statistics + tags: + - Statistics + parameters: + - name: from + in: query + description: in IS0 8601 format. eg. `1963-11-22T18:30:00Z` + required: true + schema: + type: string + format: date-time + - name: to + in: query + description: in IS0 8601 format. eg. `1963-11-22T18:30:00Z` + required: true + schema: + type: string + format: date-time + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Statistics' + /calendars: + get: + summary: Fetch a list of Calendars + description: Without params, it returns a list of Calendars the user has access to + tags: + - Calendars + parameters: + - name: all + in: query + description: Can only be used by admins or managers to fetch all entities + schema: + type: boolean + - name: userId + in: query + description: Standard users can use this only with their own _userId_ + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Calendar' + post: + summary: Create a Calendar + tags: + - Calendars + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Calendar' + required: true + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Calendar' + x-codegen-request-body-name: body + /calendars/{id}: + put: + summary: Update a Calendar + tags: + - Calendars + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Calendar' + required: true + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Calendar' + x-codegen-request-body-name: body + delete: + summary: Delete a Calendar + tags: + - Calendars + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '204': + description: No Content + content: {} + /attributes/computed: + get: + summary: Fetch a list of Attributes + description: Without params, it returns a list of Attributes the user has access to + tags: + - Attributes + parameters: + - name: all + in: query + description: Can only be used by admins or managers to fetch all entities + schema: + type: boolean + - name: userId + in: query + description: Standard users can use this only with their own _userId_ + schema: + type: integer + - name: deviceId + in: query + description: >- + Standard users can use this only with _deviceId_s, they have access + to + schema: + type: integer + - name: groupId + in: query + description: >- + Standard users can use this only with _groupId_s, they have access + to + schema: + type: integer + - name: refresh + in: query + schema: + type: boolean + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Attribute' + post: + summary: Create an Attribute + tags: + - Attributes + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Attribute' + required: true + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Attribute' + x-codegen-request-body-name: body + /attributes/computed/{id}: + put: + summary: Update an Attribute + tags: + - Attributes + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Attribute' + required: true + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Attribute' + x-codegen-request-body-name: body + delete: + summary: Delete an Attribute + tags: + - Attributes + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '204': + description: No Content + content: {} + /drivers: + get: + summary: Fetch a list of Drivers + description: Without params, it returns a list of Drivers the user has access to + tags: + - Drivers + parameters: + - name: all + in: query + description: Can only be used by admins or managers to fetch all entities + schema: + type: boolean + - name: userId + in: query + description: Standard users can use this only with their own _userId_ + schema: + type: integer + - name: deviceId + in: query + description: >- + Standard users can use this only with _deviceId_s, they have access + to + schema: + type: integer + - name: groupId + in: query + description: >- + Standard users can use this only with _groupId_s, they have access + to + schema: + type: integer + - name: refresh + in: query + schema: + type: boolean + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Driver' + post: + summary: Create a Driver + tags: + - Drivers + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Driver' + required: true + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Driver' + x-codegen-request-body-name: body + /drivers/{id}: + put: + summary: Update a Driver + tags: + - Drivers + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Driver' + required: true + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Driver' + x-codegen-request-body-name: body + delete: + summary: Delete a Driver + tags: + - Drivers + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '204': + description: No Content + content: {} + /maintenance: + get: + summary: Fetch a list of Maintenance + description: Without params, it returns a list of Maintenance the user has access to + tags: + - Maintenance + parameters: + - name: all + in: query + description: Can only be used by admins or managers to fetch all entities + schema: + type: boolean + - name: userId + in: query + description: Standard users can use this only with their own _userId_ + schema: + type: integer + - name: deviceId + in: query + description: >- + Standard users can use this only with _deviceId_s, they have access + to + schema: + type: integer + - name: groupId + in: query + description: >- + Standard users can use this only with _groupId_s, they have access + to + schema: + type: integer + - name: refresh + in: query + schema: + type: boolean + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Maintenance' + post: + summary: Create a Maintenance + tags: + - Maintenance + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Maintenance' + required: true + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Maintenance' + x-codegen-request-body-name: body + /maintenance/{id}: + put: + summary: Update a Maintenance + tags: + - Maintenance + parameters: + - name: id + in: path + required: true + schema: + type: integer + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Maintenance' + required: true + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Maintenance' + x-codegen-request-body-name: body + delete: + summary: Delete a Maintenance + tags: + - Maintenance + parameters: + - name: id + in: path + required: true + schema: + type: integer + responses: + '204': + description: No Content + content: {} +components: + schemas: + Position: + type: object + properties: + id: + type: integer + deviceId: + type: integer + protocol: + type: string + deviceTime: + type: string + description: in IS0 8601 format. eg. `1963-11-22T18:30:00Z` + format: date-time + fixTime: + type: string + description: in IS0 8601 format. eg. `1963-11-22T18:30:00Z` + format: date-time + serverTime: + type: string + description: in IS0 8601 format. eg. `1963-11-22T18:30:00Z` + format: date-time + outdated: + type: boolean + valid: + type: boolean + latitude: + type: number + longitude: + type: number + altitude: + type: number + speed: + type: number + description: in knots + course: + type: number + address: + type: string + accuracy: + type: number + network: + type: object + properties: {} + geofenceIds: + type: array + items: + type: integer + attributes: + type: object + properties: {} + User: + type: object + properties: + id: + type: integer + name: + type: string + email: + type: string + phone: + type: string + nullable: true + readonly: + type: boolean + administrator: + type: boolean + map: + type: string + nullable: true + latitude: + type: number + longitude: + type: number + zoom: + type: integer + password: + type: string + coordinateFormat: + type: string + nullable: true + disabled: + type: boolean + expirationTime: + type: string + description: in IS0 8601 format. eg. `1963-11-22T18:30:00Z` + format: date-time + nullable: true + deviceLimit: + type: integer + userLimit: + type: integer + deviceReadonly: + type: boolean + limitCommands: + type: boolean + fixedEmail: + type: boolean + poiLayer: + type: string + nullable: true + attributes: + type: object + properties: {} + Server: + type: object + properties: + id: + type: integer + registration: + type: boolean + readonly: + type: boolean + deviceReadonly: + type: boolean + limitCommands: + type: boolean + map: + type: string + bingKey: + type: string + mapUrl: + type: string + poiLayer: + type: string + latitude: + type: number + longitude: + type: number + zoom: + type: integer + version: + type: string + forceSettings: + type: boolean + coordinateFormat: + type: string + openIdEnabled: + type: boolean + openIdForce: + type: boolean + attributes: + type: object + properties: {} + Command: + type: object + properties: + id: + type: integer + deviceId: + type: integer + description: + type: string + type: + type: string + attributes: + type: object + properties: {} + Device: + type: object + properties: + id: + type: integer + name: + type: string + uniqueId: + type: string + status: + type: string + disabled: + type: boolean + lastUpdate: + type: string + description: in IS0 8601 format. eg. `1963-11-22T18:30:00Z` + format: date-time + nullable: true + positionId: + type: integer + nullable: true + groupId: + type: integer + nullable: true + phone: + type: string + nullable: true + model: + type: string + nullable: true + contact: + type: string + nullable: true + category: + type: string + nullable: true + attributes: + type: object + properties: {} + Group: + type: object + properties: + id: + type: integer + name: + type: string + groupId: + type: integer + attributes: + type: object + properties: {} + Permission: + type: object + properties: + userId: + type: integer + description: User id, can be only first parameter + deviceId: + type: integer + description: >- + Device id, can be first parameter or second only in combination with + userId + groupId: + type: integer + description: >- + Group id, can be first parameter or second only in combination with + userId + geofenceId: + type: integer + description: Geofence id, can be second parameter only + notificationId: + type: integer + description: Notification id, can be second parameter only + calendarId: + type: integer + description: >- + Calendar id, can be second parameter only and only in combination + with userId + attributeId: + type: integer + description: Computed attribute id, can be second parameter only + driverId: + type: integer + description: Driver id, can be second parameter only + managedUserId: + type: integer + description: >- + User id, can be second parameter only and only in combination with + userId + commandId: + type: integer + description: Saved command id, can be second parameter only + description: >- + This is a permission map that contain two object indexes. It is used to + link/unlink objects. Order is important. Example: { deviceId:8, + geofenceId: 16 } + CommandType: + type: object + properties: + type: + type: string + Geofence: + type: object + properties: + id: + type: integer + name: + type: string + description: + type: string + area: + type: string + calendarId: + type: integer + attributes: + type: object + properties: {} + Notification: + type: object + properties: + id: + type: integer + type: + type: string + always: + type: boolean + web: + type: boolean + mail: + type: boolean + sms: + type: boolean + calendarId: + type: integer + attributes: + type: object + properties: {} + NotificationType: + type: object + properties: + type: + type: string + Event: + type: object + properties: + id: + type: integer + type: + type: string + eventTime: + type: string + description: in IS0 8601 format. eg. `1963-11-22T18:30:00Z` + format: date-time + deviceId: + type: integer + positionId: + type: integer + geofenceId: + type: integer + maintenanceId: + type: integer + attributes: + type: object + properties: {} + ReportSummary: + type: object + properties: + deviceId: + type: integer + deviceName: + type: string + maxSpeed: + type: number + description: in knots + averageSpeed: + type: number + description: in knots + distance: + type: number + description: in meters + spentFuel: + type: number + description: in liters + engineHours: + type: integer + ReportTrips: + type: object + properties: + deviceId: + type: integer + deviceName: + type: string + maxSpeed: + type: number + description: in knots + averageSpeed: + type: number + description: in knots + distance: + type: number + description: in meters + spentFuel: + type: number + description: in liters + duration: + type: integer + startTime: + type: string + description: in IS0 8601 format. eg. `1963-11-22T18:30:00Z` + format: date-time + startAddress: + type: string + startLat: + type: number + startLon: + type: number + endTime: + type: string + description: in IS0 8601 format. eg. `1963-11-22T18:30:00Z` + format: date-time + endAddress: + type: string + endLat: + type: number + endLon: + type: number + driverUniqueId: + type: integer + driverName: + type: string + ReportStops: + type: object + properties: + deviceId: + type: integer + deviceName: + type: string + duration: + type: integer + startTime: + type: string + description: in IS0 8601 format. eg. `1963-11-22T18:30:00Z` + format: date-time + address: + type: string + lat: + type: number + lon: + type: number + endTime: + type: string + description: in IS0 8601 format. eg. `1963-11-22T18:30:00Z` + format: date-time + spentFuel: + type: number + description: in liters + engineHours: + type: integer + Statistics: + type: object + properties: + captureTime: + type: string + description: in IS0 8601 format. eg. `1963-11-22T18:30:00Z` + format: date-time + activeUsers: + type: integer + activeDevices: + type: integer + requests: + type: integer + messagesReceived: + type: integer + messagesStored: + type: integer + DeviceAccumulators: + type: object + properties: + deviceId: + type: integer + totalDistance: + type: number + description: in meters + hours: + type: number + Calendar: + type: object + properties: + id: + type: integer + name: + type: string + data: + type: string + description: base64 encoded in iCalendar format + attributes: + type: object + properties: {} + Attribute: + type: object + properties: + id: + type: integer + description: + type: string + attribute: + type: string + expression: + type: string + type: + type: string + description: String|Number|Boolean + Driver: + type: object + properties: + id: + type: integer + name: + type: string + uniqueId: + type: string + attributes: + type: object + properties: {} + Maintenance: + type: object + properties: + id: + type: integer + name: + type: string + type: + type: string + start: + type: number + period: + type: number + attributes: + type: object + properties: {} + parameters: + entityId: + name: id + in: path + required: true + schema: + type: integer + all: + name: all + in: query + description: Can only be used by admins or managers to fetch all entities + schema: + type: boolean + refresh: + name: refresh + in: query + schema: + type: boolean + userId: + name: userId + in: query + description: Standard users can use this only with their own _userId_ + schema: + type: integer + deviceId: + name: deviceId + in: query + description: Standard users can use this only with _deviceId_s, they have access to + schema: + type: integer + groupId: + name: groupId + in: query + description: Standard users can use this only with _groupId_s, they have access to + schema: + type: integer + deviceIdArray: + name: deviceId + in: query + style: form + explode: true + schema: + type: array + items: + type: integer + groupIdArray: + name: groupId + in: query + style: form + explode: true + schema: + type: array + items: + type: integer + fromTime: + name: from + in: query + description: in IS0 8601 format. eg. `1963-11-22T18:30:00Z` + required: true + schema: + type: string + format: date-time + toTime: + name: to + in: query + description: in IS0 8601 format. eg. `1963-11-22T18:30:00Z` + required: true + schema: + type: string + format: date-time + requestBodies: + Device: + content: + application/json: + schema: + $ref: '#/components/schemas/Device' + required: true + Permission: + content: + application/json: + schema: + $ref: '#/components/schemas/Permission' + required: true + Group: + content: + application/json: + schema: + $ref: '#/components/schemas/Group' + required: true + User: + content: + application/json: + schema: + $ref: '#/components/schemas/User' + required: true + Geofence: + content: + application/json: + schema: + $ref: '#/components/schemas/Geofence' + required: true + Calendar: + content: + application/json: + schema: + $ref: '#/components/schemas/Calendar' + required: true + Attribute: + content: + application/json: + schema: + $ref: '#/components/schemas/Attribute' + required: true + Driver: + content: + application/json: + schema: + $ref: '#/components/schemas/Driver' + required: true + Command: + content: + application/json: + schema: + $ref: '#/components/schemas/Command' + required: true + Notification: + content: + application/json: + schema: + $ref: '#/components/schemas/Notification' + required: true + Maintenance: + content: + application/json: + schema: + $ref: '#/components/schemas/Maintenance' + required: true + securitySchemes: + BasicAuth: + type: http + scheme: basic + ApiKey: + type: http + scheme: bearer diff --git a/reqirements.txt b/reqirements.txt new file mode 100644 index 0000000..8268981 --- /dev/null +++ b/reqirements.txt @@ -0,0 +1,2 @@ +kivy +cryptography \ No newline at end of file diff --git a/resources/credentials.enc b/resources/credentials.enc new file mode 100644 index 0000000..c7d9fe0 --- /dev/null +++ b/resources/credentials.enc @@ -0,0 +1 @@ +gAAAAABoPZqrY8gNIQXxQ_TAwdtbkZWX-zhwrx7hLuBFXH0M47fThQkAAG0vi8pMJhRnFipZ7t2Nj7SoVhjQqwHo4WOs7IH7lJCKSReeQT-1MdB0fYG16foBJpxK29kz-F9YRjCELQ0m \ No newline at end of file diff --git a/resources/key.key b/resources/key.key new file mode 100644 index 0000000..ede8923 --- /dev/null +++ b/resources/key.key @@ -0,0 +1 @@ +wetp_PNG9CC5432-W9H3rUbaqIurwldZxHlOgori5kY= \ No newline at end of file diff --git a/resources/server_settings.enc b/resources/server_settings.enc new file mode 100644 index 0000000..7eabde5 --- /dev/null +++ b/resources/server_settings.enc @@ -0,0 +1 @@ +gAAAAABoPZo4DMcBt7anBtvn5wMfvWpsjR4Zi1BPdQhvnvSj0SyQmrQEF1uhsUjf0Y6Le3vBPKSplUypYURK_BUlJMbxUh2yI93KCcXdMJIy23TyNlVlt7dv1mPN3aQAAd4TqhA_dEVPAfBtT5ckcQAXqCRwd1RjRp06Qgz3XNHeLs14MxGet0rbWCLppopr6BTGulvbAdvCFoDqmRFYbfyWYlqqsN4-ZF0Q0qObOVPn7hqYHFTenK6tidi-JOx7Lx0jrCZWA2V0OgwCZOsTA-BYZeWK8RI2U645SJ_DBTBwK2avb6wDduJKOOIKJWKdUF5HaN3Ffc61KWxYO_8Eb21bYmNsDB31l9n-OgJLdzStT-WqLMmmw027YG_SZJsv-P2Ce0lCaw29s2Lbh_2VxWShJW6aDRxibZgHK3CoPajqR0MAkGJxT2w7pt7wT1zeMKHu51q3yPpV \ No newline at end of file diff --git a/resources/track.png b/resources/track.png new file mode 100644 index 0000000000000000000000000000000000000000..eb097c15b2c79eb4b3b062b1d77beeda5ae13334 GIT binary patch literal 23869 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 D&c~p2 literal 0 HcmV?d00001 diff --git a/test_traccar_trip.py b/test_traccar_trip.py new file mode 100644 index 0000000..e79785b --- /dev/null +++ b/test_traccar_trip.py @@ -0,0 +1,80 @@ +import requests + +def get_positions(server_url, token, device_id=None, from_time=None, to_time=None): + """ + Fetch position information from the Traccar server. + + Args: + server_url (str): The URL of the Traccar server. + token (str): The authentication token. + device_id (int, optional): The ID of the device. If not provided, fetches positions for all devices. + from_time (str, optional): The start time in ISO 8601 format (e.g., '2024-04-05T00:00:00Z'). + to_time (str, optional): The end time in ISO 8601 format (e.g., '2024-04-05T23:59:59Z'). + + Returns: + list: The position information. + """ + # Ensure the server_url has a valid scheme + if not server_url.startswith("http://") and not server_url.startswith("https://"): + server_url = f"https://{server_url}" # Default to https:// if no scheme is provided + + # Set the Authorization header with the token + headers = {"Authorization": f"Bearer {token}"} + + # API endpoint for fetching positions + url = f"{server_url}/positions" + + # Request payload + payload = {} + if device_id: + payload["deviceId"] = device_id + if from_time and to_time: + payload["from"] = from_time + payload["to"] = to_time + + try: + # Log the payload for debugging + print(f"Request Payload: {payload}") + + # Make the API request + response = requests.get(url, params=payload, headers=headers) + + # Log the response status and content for debugging + print(f"Response Status Code: {response.status_code}") + print(f"Response Content: {response.text}") + + # Check if the response was successful + if response.status_code == 200: + positions = response.json() + print(f"Retrieved {len(positions)} positions:") + for position in positions: + print(position) + return positions + elif response.status_code == 400: + print("Bad Request: Please check the request payload and token.") + return None + else: + print(f"Failed to fetch positions: {response.status_code} - {response.reason}") + return None + except requests.exceptions.RequestException as e: + print(f"Error fetching positions: {str(e)}") + return None + + +# Test the function +if __name__ == "__main__": + # Manually enter the server URL and token + server_url = input("Enter the server URL (e.g., https://gps.moto-adv.com/api): ").strip() + token = input("Enter the authentication token: ").strip() + + # Optional: Enter device ID and date range + device_id = input("Enter the device ID (leave blank for all devices): ").strip() + device_id = int(device_id) if device_id else None + from_time = input("Enter the start time (ISO 8601, e.g., 2024-04-05T00:00:00Z, leave blank for none): ").strip() + to_time = input("Enter the end time (ISO 8601, e.g., 2024-04-05T23:59:59Z, leave blank for none): ").strip() + + positions = get_positions(server_url, token, device_id, from_time, to_time) + if positions: + print("Position data retrieved successfully!") + else: + print("No position data found or an error occurred.") \ No newline at end of file diff --git a/traccar.kv b/traccar.kv new file mode 100644 index 0000000..cbe4143 --- /dev/null +++ b/traccar.kv @@ -0,0 +1,347 @@ +: + BoxLayout: + orientation: "vertical" + padding: 20 + spacing: 20 + canvas.before: + Color: + rgba: 0.11, 0.10, 0.15, 1 # Background color: #1C1A27 + Rectangle: + pos: self.pos + size: self.size + + Image: + source: "resources/track.png" + size_hint: (1, 0.66) # 2/3 of the screen height + + TextInput: + id: username_input + hint_text: "Username" + multiline: False + font_size: 20 # Text size + height: self.minimum_height + size_hint_y: None # Fix height + size_hint_x: 0.8 # Center horizontally + pos_hint: {"center_x": 0.5} + + TextInput: + id: password_input + hint_text: "Password" + multiline: False + password: True + font_size: 20 # Text size + height: self.minimum_height + size_hint_y: None # Fix height + size_hint_x: 0.8 # Center horizontally + pos_hint: {"center_x": 0.5} + + BoxLayout: + orientation: "horizontal" + size_hint_y: None + height: 50 # Fixed height for the button group + size_hint_x: 0.8 # Match the width of the TextInput fields + pos_hint: {"center_x": 0.5} # Center horizontally + spacing: 10 + + Button: + text: "Login" + font_size: 20 # Match the font size of the TextInput fields + size_hint_x: 0.5 # Half the width of the BoxLayout + size_hint_y: None + height: 50 # Fixed height for the button + background_color: 0.341, 0.235, 0.980, 1 # Purple color (#573CFA) + on_press: root.login() + + Button: + text: "Register" + font_size: 20 + size_hint_x: 0.5 + size_hint_y: None + height: 50 + background_color: 0.341, 0.235, 0.980, 1 # Purple color (#573CFA) + on_press: app.root.current = "register" # Navigate to the RegisterScreen + +: + BoxLayout: + orientation: "vertical" + padding: 20 + spacing: 20 + canvas.before: + Color: + rgba: 0.11, 0.10, 0.15, 1 # Background color: #1C1A27 + Rectangle: + pos: self.pos + size: self.size + + Image: + source: "resources/track.png" + size_hint: (1, 0.66) # 2/3 of the screen height + + TextInput: + id: set_username_input + hint_text: "Set Username" + multiline: False + font_size: 20 # Text size + height: self.minimum_height + size_hint_y: None # Fix height + size_hint_x: 0.8 # Center horizontally + pos_hint: {"center_x": 0.5} + + TextInput: + id: set_password_input + hint_text: "Set Password" + multiline: False + password: True + font_size: 20 # Text size + height: self.minimum_height + size_hint_y: None # Fix height + size_hint_x: 0.8 # Center horizontally + pos_hint: {"center_x": 0.5} + + TextInput: + id: confirm_password_input + hint_text: "Confirm Password" + multiline: False + password: True + font_size: 20 # Text size + height: self.minimum_height + size_hint_y: None # Fix height + size_hint_x: 0.8 # Center horizontally + pos_hint: {"center_x": 0.5} + + TextInput: + id: set_email_input + hint_text: "Set Email" + multiline: False + font_size: 20 # Text size + height: self.minimum_height + size_hint_y: None # Fix height + size_hint_x: 0.8 # Center horizontally + pos_hint: {"center_x": 0.5} + + + + Button: + text: "Create Username" + font_size: 20 # Match the font size of the TextInput fields + size_hint_x: 0.8 # Match the width of the TextInput fields + size_hint_y: None + height: 50 # Fixed height for the button + pos_hint: {"center_x": 0.5} + on_press: root.create_user() + + Button: + text: "Back to Login" + font_size: 20 + size_hint_x: 0.8 + size_hint_y: None + height: 50 + pos_hint: {"center_x": 0.5} + on_press: app.root.current = "login" + + Label: + id: result_label + text: "" + size_hint: (1, 0.2) + +: + BoxLayout: + orientation: "vertical" + padding: 20 + spacing: 20 + canvas.before: + Color: + rgba: 0.11, 0.10, 0.15, 1 # Background color: #1C1A27 + Rectangle: + pos: self.pos + size: self.size + + # First row: Server settings + BoxLayout: + id: server_info_settings + orientation: "horizontal" + size_hint_y: None + height: 30 + spacing: 10 + canvas.before: + Color: + rgba: root.server_box_color # Dynamic color for the box + Rectangle: + pos: self.pos + size: self.size + + Label: + id: server_info_label + text: root.server_info_text # Dynamic text for the label + font_size: 14 # Reduced font size + size_hint_x: 0.8 + + Button: + text: "Settings" + size_hint_x: 0.2 + background_color: 0.341, 0.235, 0.980, 1 # Purple color (#573CFA) + on_press: app.root.current = "settings" + + # Second row: Frame for device and date selection + BoxLayout: + orientation: "vertical" + size_hint_y: None + height: 120 # Adjusted height for the frame + spacing: 10 + padding: 10 + canvas.before: + Color: + rgba: 0.2, 0.2, 0.2, 1 # Frame background color + Rectangle: + pos: self.pos + size: self.size + + Label: + text: "Select device and the day of the data" # Main label + font_size: 14 # Reduced font size + size_hint_y: None + height: 20 + + BoxLayout: + orientation: "horizontal" + size_hint_y: None + height: 30 + spacing: 10 + + Spinner: + id: devices_spinner + text: "Loading devices..." # Default text + values: [] # Initially empty + size_hint: (0.5, None) + height: 25 + font_size: 14 # Reduced font size + on_text: root.on_device_selected(self.text) # Trigger background color change + + Button: + id: date_picker_button + text: "Select Date" + size_hint: (0.5, None) + height: 25 + font_size: 14 # Reduced font size + on_press: root.open_date_picker() + + # New row: Get trip server data button + Button: + id: get_trip_data_button + text: "Get trip server data" + size_hint: (1, None) + height: 30 + font_size: 14 + background_color: 0.341, 0.235, 0.980, 1 # Purple color (#573CFA) + on_press: root.get_trip_server_data() + + # Third row: Result label + Label: + id: result_label + text: "Welcome to the Home Screen!" + font_size: 14 # Reduced font size + size_hint: (1, 0.8) + +: + BoxLayout: + orientation: "vertical" + padding: 20 + spacing: 20 + canvas.before: + Color: + rgba: 0.11, 0.10, 0.15, 1 # Background color: #1C1A27 + Rectangle: + pos: self.pos + size: self.size + + Image: + source: "resources/track.png" + size_hint: (1, 0.66) # 2/3 of the screen height + + TextInput: + id: server_url_input + hint_text: "Traccar Server URL" + multiline: False + font_size: 20 + size_hint_y: None + height: self.minimum_height + size_hint_x: 0.8 + pos_hint: {"center_x": 0.5} + + TextInput: + id: username_input + hint_text: "Username" + multiline: False + font_size: 20 + size_hint_y: None + height: self.minimum_height + size_hint_x: 0.8 + pos_hint: {"center_x": 0.5} + + TextInput: + id: password_input + hint_text: "Password" + multiline: False + password: True + font_size: 20 + size_hint_y: None + height: self.minimum_height + size_hint_x: 0.8 + pos_hint: {"center_x": 0.5} + + TextInput: + id: token_input + hint_text: "Token" + multiline: False + font_size: 20 + size_hint_y: None + height: self.minimum_height + size_hint_x: 0.8 + pos_hint: {"center_x": 0.5} + + BoxLayout: + orientation: "horizontal" + size_hint_y: None + height: 50 + size_hint_x: 0.8 + pos_hint: {"center_x": 0.5} + spacing: 10 + + Button: + text: "Test Connection" + size_hint_x: 0.5 # Half the width of the BoxLayout + size_hint_y: None + height: 50 + background_color: 0.341, 0.235, 0.980, 1 # Purple color (#573CFA) + on_press: root.test_connection() + + Button: + text: "Save Settings" + size_hint_x: 0.5 # Half the width of the BoxLayout + size_hint_y: None + height: 50 + background_color: 0.008, 0.525, 0.290, 1 # Purple color (#573CFA) + on_press: root.save_settings() + + Label: + id: result_label + text: "Waiting to test connection..." + size_hint_y: None + height: 50 + size_hint_x: 0.8 + pos_hint: {"center_x": 0.5} + canvas.before: + Color: + rgba: 1, 1, 1, 1 # White color + Line: + width: 1.5 + rectangle: self.x, self.y, self.width, self.height + + Button: + text: "Return to Home" + size_hint_y: None + height: 50 + size_hint_x: 0.8 + pos_hint: {"center_x": 0.5} + background_color: 0.341, 0.235, 0.980, 1 # Purple color (#573CFA) + on_press: app.root.current = "home" \ No newline at end of file diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..53cf76a --- /dev/null +++ b/utils.py @@ -0,0 +1,220 @@ +import os # Import the os module +import json +import requests +from cryptography.fernet import Fernet + +# Define the path to the server settings file +RESOURCES_FOLDER = "resources" +SERVER_SETTINGS_FILE = os.path.join(RESOURCES_FOLDER, "server_settings.enc") + +def load_key(): + """Load the encryption key from the key file.""" + key_file_path = os.path.join(RESOURCES_FOLDER, "key.key") + if not os.path.exists(key_file_path): + raise FileNotFoundError("Encryption key file not found.") + with open(key_file_path, "rb") as key_file: + return key_file.read() + +def decrypt_data(encrypted_data): + """Decrypt the encrypted data using the loaded key.""" + key = load_key() # Load the key from the file + fernet = Fernet(key) + return fernet.decrypt(encrypted_data).decode() + +def check_server_settings(): + """ + Load server settings from a configuration file or encrypted storage. + + Returns: + dict: A dictionary containing server settings (e.g., server_url, username, password, token). + """ + settings_file = os.path.join("resources", "server_settings.enc") + if not os.path.exists(settings_file): + print("Settings file not found.") + return None + + try: + with open(settings_file, "rb") as file: + encrypted_data = file.read() + decrypted_data = decrypt_data(encrypted_data) + settings = json.loads(decrypted_data) + return settings + except Exception as e: + print(f"Error loading settings: {str(e)}") + return None + +def get_devices_from_server(): + """ + Retrieve a list of devices from the Traccar server and create a mapping of device names to IDs. + + Returns: + dict: A dictionary mapping device names to their IDs if the request is successful. + None: If the request fails. + """ + # Check if the server settings file exists + settings = check_server_settings() + if not settings: + return None + + try: + # Extract server details + server_url = settings.get("server_url") + token = settings.get("token") # Optional, if token is used for authentication + + if not server_url: + print("Error: Missing server URL in server_settings.enc.") + return None + + # Ensure the server_url has a valid scheme + if not server_url.startswith("http://") and not server_url.startswith("https://"): + server_url = f"https://{server_url}" # Default to https:// if no scheme is provided + + # Determine authentication method + headers = {"Authorization": f"Bearer {token}"} if token else None + + if not token: + print("Error: Missing authentication details (token).") + return None + + # Make a GET request to the /devices endpoint + response = requests.get(f"{server_url}/api/devices", headers=headers) + + # Check the response status + if response.status_code == 200: + print("Devices retrieved successfully!") + devices = response.json() # Get the list of devices + + # Create a mapping of device names to IDs + device_mapping = {device.get("name", "Unnamed Device"): device.get("id", "Unknown ID") for device in devices} + + # Debugging: Print the mapping + for name, device_id in device_mapping.items(): + print(f"Device Name: {name}, Device ID: {device_id}") + + return device_mapping # Return the mapping of device names to IDs + else: + print(f"Error: {response.status_code} - {response.reason}") + return None + + except Exception as e: + print(f"Error retrieving devices: {str(e)}") + return None + +def get_route_info(device_id, selected_date): + """ + Fetch route information for a specific device and date from the Traccar server. + + Args: + device_id (int): The ID of the device. + selected_date (str): The selected date in the format 'YYYY-MM-DD'. + + Returns: + list: The route information for the device on the selected date. + """ + # Load server settings + settings = check_server_settings() + if not settings: + print("Error: Unable to load server settings.") + return None + + # Extract server details + server_url = settings.get("server_url") + token = settings.get("token") + + if not server_url: + print("Error: Missing server URL in settings.") + return None + + if not token: + print("Error: Missing token in settings.") + return None + + # Ensure the server_url has a valid scheme + if not server_url.startswith("http://") and not server_url.startswith("https://"): + server_url = f"https://{server_url}" # Default to https:// if no scheme is provided + + # Set the Authorization header with the token + headers = {"Authorization": f"Bearer {token}"} + + # Convert the selected date to ISO 8601 format for the API + start_time = f"{selected_date}T00:00:00Z" + end_time = f"{selected_date}T23:59:59Z" + + # API endpoint for fetching route reports + url = f"{server_url}/reports/route" + + # Request payload + payload = { + "deviceId": device_id, + "from": start_time, + "to": end_time, + } + + try: + # Log the payload for debugging + print(f"Request Payload: {payload}") + + # Make the API request + response = requests.get(url, params=payload, headers=headers) + + # Log the response status and content for debugging + print(f"Response Status Code: {response.status_code}") + print(f"Response Content: {response.text}") + + # Check if the request was successful + if response.status_code == 200: + route = response.json() + print(f"Route for device {device_id} on {selected_date}:") + for position in route: + print(position) + return route + elif response.status_code == 400: + print("Bad Request: Please check the request payload and token.") + return None + else: + print(f"Failed to fetch route: {response.status_code} - {response.reason}") + return None + except requests.exceptions.RequestException as e: + print(f"Error fetching route: {str(e)}") + return None + +def test_connection(server_url, username=None, password=None, token=None): + """ + Test the connection with the Traccar server. + + Args: + server_url (str): The URL of the Traccar server. + username (str, optional): The username for basic authentication. + password (str, optional): The password for basic authentication. + token (str, optional): The token for bearer authentication. + + Returns: + dict: A dictionary containing the connection status and message. + """ + if not server_url: + return {"status": False, "message": "Please provide the server URL."} + + if not token and (not username or not password): + return {"status": False, "message": "Please provide either a token or username and password."} + + try: + # Determine authentication method + headers = {"Authorization": f"Bearer {token}"} if token else None + auth = None if token else (username, password) + + # Make a GET request to the server + response = requests.get(f"{server_url}/api/server", headers=headers, auth=auth, timeout=10) + + if response.status_code == 200: + return {"status": True, "message": "Connection successful! Server is reachable."} + else: + return {"status": False, "message": f"Error: {response.status_code} - {response.reason}"} + except requests.exceptions.Timeout: + return {"status": False, "message": "Connection timed out. Please try again."} + except requests.exceptions.RequestException as e: + return {"status": False, "message": f"Connection failed: {str(e)}"} + +# Call the function +test_device_id = 1 # Replace with the device ID from the spinner +test_date = "2025-06-01" # Replace with the selected date from the date picker +get_route_info(test_device_id, test_date)