From 2b0972a22cbaffd3c03211a2ab901d49f9915c41 Mon Sep 17 00:00:00 2001 From: Anders Knutsen Date: Wed, 3 Dec 2025 09:51:31 +0100 Subject: [PATCH] v.0.9 Beta --- README.md | 718 +++++++++++++++++++++++++++++++------- xml_combine_for_import.py | 1 - 2 files changed, 596 insertions(+), 123 deletions(-) diff --git a/README.md b/README.md index 1b6b4d3..f05461c 100644 --- a/README.md +++ b/README.md @@ -1,199 +1,444 @@ # Patriot API -The **Patriot API** is a multi-tenant REST API used to manage clients, zones, and users and automatically generate XML files in the format required by Patriot. +The **Alarm24 Patriot API** is a REST API used to manage clients, zones, and users in the format required by Patriot. -Each API key has **its own fully isolated dataset**. +--- ## πŸ” Authentication -All requests require: +Every request must include a authorization header: -``` +```http Authorization: Bearer ``` -If the key is missing, invalid, disabled, or expired β†’ **401 Unauthorized** +If the key is missing, invalid, disabled, or expired β†’ **401 Unauthorized**. --- ## πŸ“˜ Common Status Codes -| Status | Meaning | -|--------|---------| -| **200 OK** | Request successful (read or update) | -| **201 Created** | Resource successfully created | -| **204 No Content** | Resource deleted | -| **401 Unauthorized** | Invalid/missing API key | -| **404 Not Found** | Client, zone, or user not found | -| **409 Conflict** | Duplicate user number | -| **422 Unprocessable Entity** | Validation failed | +| Status | Meaning | +|--------|------------------------------------------------| +| **200 OK** | Request successful | +| **201 Created** | Resource created | +| **204 No Content** | Resource deleted | +| **401 Unauthorized** | Missing/invalid/disabled/expired API key | +| **404 Not Found** | Client/zone/user not found | +| **409 Conflict** | Duplicate record, or client temporarily locked | +| **422 Unprocessable Entity** | Validation failed, check request format | --- ## 🧩 Required Headers -Some GET requests require client/user IDs in headers: +Some GET endpoints require IDs in HTTP headers: | Header | Purpose | |--------|---------| | **X-Client-Id** | Selects the client | -| **X-User-No** | Selects a specific user | +| **X-User-No** | Selects a specific user | + +All endpoints **also require** the `Authorization: Bearer` header. + +--- + +## πŸ—„ Data Isolation & XML Generation + +### Per-key Data Isolation + +Each API key has its **own completely separate dataset**: + +- Clients, zones, and users are not shared between API keys. +- Two different keys can both have a client `1234`, but those are **separate clients** internally. + +### XML Output Structure + +For each API key, XML files for new clients are written to temporary storage before import: + +```text +out/clients//.xml +``` + +Example: + +```text +out/clients/Alarm24Key/9998.xml +out/clients/InstallerXKey/1234.xml +``` + +### `<__id>` with Port Suffix + +Each key in `keys.json` has a `port` field, e.g.: + +```json +{ + "key_name": "Alarm24", + "key": "secure-api-key", + "enabled": true, + "valid_to": "2030-01-01T00:00:00Z", + "port": "BASE11" +} +``` + +When generating XML, the `<__id>` element is: + +```xml +<__id>9998BASE11 +``` + +- `9998` = client ID +- `BASE11` = `port` value from `keys.json` + +The **XML filename** remains `9998.xml` (client id only). + +### Extra XML Fields Injected Per Key + +Some XML fields are **not provided by the API**, but are automatically injected from `keys.json` for each key: + +Examples (exact names depend on your `keys.json`): + +```xml +Installer Name + +True +False +TLF01BASE01 + +True +False +TLF02BASE01 + +True +CID4BASE01 +None +SIADecimal + + + + Alarm24 + Alarm24 Tilgang + True + + + Import + Østfold Sikkerhetsservice AS + True + + +``` + +These are **configured per key** and not exposed as API fields, and can therefore not be changed by the client. + +### Auto-added Installer User (UserNo 199) + +For each client, the XML will also contain an additional user (not managed via API): + +```xml + + Installer Name + installer@email.com + 199 + 0 + N + +``` + +The actual `User_Name` and `Email` come from the API key configuration (`keys.json`). +This is the clients installer company. Make sure the name and e-mail is identical to whats stored in Patriot, otherwise it will generate a new installer on first import. --- # πŸ“‚ API Endpoints -## ======================= +Below are the available endpoints and request/response formats. + +> **Note:** All examples assume you are also sending the correct `Authorization: Bearer ` header. + +--- + ## πŸ”Ή CLIENTS -## ======================= -## **GET /clients** -Get full client info. +### GET `/clients` -Requires header: +Get full client info (info + zones + users). +Also **regenerates XML** for this client if the XML file is missing (Only when the client is not present in DB, and is pending import). + +**Headers:** + +```http +X-Client-Id: 9998 +Authorization: Bearer ``` -X-Client-Id: + +**Response (example):** + +```json +{ + "client_id": 9998, + "info": { ... }, + "zones": { ... }, + "users": { ... } +} ``` --- -## **PUT /clients** -Create or update a client (UPSERT). +### PUT `/clients` -Returns: -- `201 Created` if new -- `200 OK` if updated +Create or update a client (**UPSERT**). -Example JSON: +- Returns **201 Created** on first creation. +- Returns **200 OK** on subsequent updates. -``` +**Body (all supported fields):** + +```json { - "client_id": 1234, + "client_id": 9998, "info": { - "Name": "Customer Name", - "Location": "Address line 1", + "Name": "Ola Nordmann", + "Alias": "", + "Location": "Produksjonsveien 13", + "area_code": "1604", - "area": "City", - "AltLookup": true, - "AltAlarmNo": "SIA1000101", - "ConvertType": "None", - "NoSigsMon": "None", - "SinceDays": 0, + "area": "Fredrikstad", + + "BusPhone": "69310000", + "Email": "post@ostsik.no", + "OKPassword": "franzjager", + "SpecRequest": "Dette skal gjΓΈres ved alarm pΓ₯ denne kunden.", + + "NoSigsMon": 1, + "SinceDays": 1, "SinceHrs": 0, - "SinceMins": 0, + "SinceMins": 30, + "ResetNosigsIgnored": true, - "ResetNosigsDays": 0, + "ResetNosigsDays": 7, "ResetNosigsHrs": 0, "ResetNosigsMins": 0, - "PanelName": "Panel Type", - "PanelSite": "Panel location" + + "InstallDateTime": "2023-02-20", + + "PanelName": "Ajax", + "PanelSite": "Stue", + "KeypadLocation": "Inngang", + "SPPage": "Ekstra informasjon som kan vΓ¦re relevant." } } ``` +Some fields may be auto-defaulted if omitted, depending on implementation. + --- -## **PATCH /clients** -Partial update of a client. +### PATCH `/clients` -Example updating only the location: +Partial update of client info. Only the fields inside `info` that you send will be updated; the rest are left unchanged. -``` +**Example: update only the Location:** + +```json { - "client_id": 1234, + "client_id": 9998, "info": { - "Location": "New Street 99" + "Location": "Ny gate 99" + } +} +``` + +You can also update any combination of supported fields, for example: + +```json +{ + "client_id": 9998, + "info": { + "BusPhone": "12345678", + "SPPage": "Oppdatert tekst" } } ``` --- -## **DELETE /clients** -Delete entire client and its XML file. +### DELETE `/clients` -``` +Delete an entire client and its XML file for the current API key. +This does **not** touch clients of other API keys, even if they use the same `client_id`. + +**Body:** + +```json { - "client_id": 1234 + "client_id": 9998 } ``` +Response: **204 No Content** on success. + --- -# ======================= -# πŸ”Ή ZONES -# ======================= +## πŸ”Ή ZONES + +### GET `/clients/zones` -## **GET /clients/zones** List all zones for a client. -Requires: +**Headers:** + +```http +X-Client-Id: 9998 +Authorization: Bearer ``` -X-Client-Id: + +**Response example:** + +```json +{ + "client_id": 9998, + "zones": [ + { + "Zone_No": 1, + "Zone_area": "Inngang", + "ModuleNo": 0 + }, + { + "Zone_No": 2, + "Zone_area": "Stue", + "ModuleNo": 0 + } + ] +} ``` --- -## **PUT /clients/zones** -Create or update a zone. +### POST `/clients/zones/list` -Example: +Alternative way to list zones using JSON body. -``` +**Body:** + +```json { - "client_id": 1234, + "client_id": 9998 +} +``` + +--- + +### PUT `/clients/zones` + +Create or update (UPSERT) a zone. + +**Body:** + +```json +{ + "client_id": 9998, "zone_id": 1, - "Zone_area": "Entrance", + "Zone_area": "Inngang", "ModuleNo": 0 } ``` +- `zone_id` must be between **1** and **999**. +- Returns **201 Created** for both create and update (by design). + --- -## **DELETE /clients/zones** -Delete a zone. +### DELETE `/clients/zones` -``` +Delete a specific zone for a client. + +**Body:** + +```json { - "client_id": 1234, + "client_id": 9998, "zone_id": 1 } ``` +Response: **204 No Content** on success. + +--- + +## πŸ”Ή USERS + +### GET `/clients/users` + +List all users for a client. + +**Headers:** + +```http +X-Client-Id: 9998 +Authorization: Bearer +``` + +**Response example:** + +```json +{ + "client_id": 9998, + "users": [ + { + "User_Name": "User Number 1", + "MobileNo": "+4712345678", + "MobileNoOrder": 1, + "Email": "user@email.com", + "Type": "U", + "UserNo": 1, + "Instructions": "Optional", + "CallOrder": 0 + } + ] +} +``` + +> Note: The installer user (UserNo 199) is **in XML only** and not exposed via this API. + +--- + +### GET `/clients/users/single` + +Get a single user by number. + +**Headers:** + +```http +X-Client-Id: 9998 +X-User-No: 1 +Authorization: Bearer +``` + +--- + +### POST `/clients/users/list` + +List all users via JSON body. + +**Body:** + +```json +{ + "client_id": 9998 +} +``` + --- -# ======================= -# πŸ”Ή USERS -# ======================= +### POST `/clients/users` -## **GET /clients/users** -List all users for a client. - -Requires: -``` -X-Client-Id: -``` - ---- - -## **GET /clients/users/single** -Get one user. - -Headers required: - -``` -X-Client-Id: -X-User-No: -``` - ---- - -## **POST /clients/users** Create a new user. -``` +**Body:** + +```json { - "client_id": 1234, + "client_id": 9998, "user": { "User_Name": "User Number 1", "MobileNo": "+4712345678", @@ -207,14 +452,34 @@ Create a new user. } ``` +If `UserNo` already exists β†’ **409 Conflict**. + --- -## **PUT /clients/users** -Update a user (full replace). +### POST `/clients/users/get` -``` +Get one specific user using JSON body. + +**Body:** + +```json { - "client_id": 1234, + "client_id": 9998, + "user_no": 1 +} +``` + +--- + +### PUT `/clients/users` + +Update/replace a user completely. + +**Body:** + +```json +{ + "client_id": 9998, "user": { "User_Name": "Changed Name", "MobileNo": "+4798765432", @@ -222,41 +487,250 @@ Update a user (full replace). "Email": "new@email.com", "Type": "U", "UserNo": 1, - "Instructions": "Updated", + "Instructions": "New instructions", "CallOrder": 0 } } ``` +User is identified by `client_id` + `UserNo`. + --- -## **DELETE /clients/users** -Delete a user. +### DELETE `/clients/users` -``` +Delete a specific user. + +**Body:** + +```json { - "client_id": 1234, + "client_id": 9998, "user_no": 1 } ``` ---- - -# πŸš€ Notes - -### βœ” Every API key has completely isolated data -Two different keys can both store client `1234`, but they are **stored separately**. - -### βœ” XML files stored per key -Example output directory: - -``` -out/clients/KeyA/1234.xml -out/clients/KeyB/1234.xml -``` - -### βœ” Deleting a client only deletes it for that API key +Response: **204 No Content** on success. --- -# πŸ“„ End of README +You can run the API as a **systemd service** (recommended) or screen/tmux process. + +Copy "main.py" and "run_server.py" to (`/opt/patriot_api/`) + +Example systemd unit (`/etc/systemd/system/patriot-api.service`): + +```ini +[Unit] +Description=Patriot Client Registry API +After=network.target + +[Service] +# Run as this user +User=root +Group=root + +WorkingDirectory=/opt/patriot_api + +# Environment variables (optional overrides) +#Environment=DATA_FILE=/opt/patriot_api/data.json +Environment=KEY_FILE=/opt/patriot_api/keys.json +Environment=XML_DIR=/opt/patriot_api/out/clients + +# Start uvicorn from the venv +ExecStart=/opt/patriot_api/.venv/bin/uvicorn main:app --host 0.0.0.0 --port 8081 --proxy-headers + +# Restart behavior +Restart=always +RestartSec=3 + +# Logging (stdout/stderr go to journalctl) +StandardOutput=journal +StandardError=journal + +[Install] +WantedBy=multi-user.target + +``` + +Then: + +```bash +sudo systemctl daemon-reload +sudo systemctl enable patriot-api.service +sudo systemctl start patriot-api.service +``` + +Logs will be available both in: + +- `/opt/patriot_api/api_requests.log` +- `journalctl -u patriot-api.service -f` + +--- + +# πŸ” Companion Service: XML Combiner & SMB Uploader + +In addition to the API, there is a separate Python script that: + +1. Scans all per-client XML files from the API: + ```text + /opt/patriot_api/out/clients/**/.xml + ``` +2. Combines them into a single `clients.xml` file under: + ```text + /opt/patriot_api/ready_for_import/clients.xml + ``` +3. Uploads this combined `clients.xml` to a Windows SMB share. +4. Tracks import state per client in `client_state.json`. +5. Logs missing or failed imports based on a results file. + +The script is designed to run **as a daemon**, sleeping until ~1 minute before the next full hour, then performing a combine/upload run. + +## Script Location & Paths + +Example paths used in the script: + +```python +XML_ROOT_PATH = Path("/opt/patriot_api/out/clients") # per-client XMLs from API +READY_DIR = Path("/opt/patriot_api/ready_for_import") # combined XML output +COMBINED_FILENAME = "clients.xml" + +LOG_FILE = "/opt/patriot_api/xml_combine.log" +ERROR_LOG_FILE = "/opt/patriot_api/import_errors.log" + +CLIENT_STATE_FILE = Path("/opt/patriot_api/client_state.json") +``` + +### Client State Tracking + +The script maintains a JSON file: + +```text +/opt/patriot_api/client_state.json +``` + +After a successful combine+upload, every client `<__id>` in that batch is marked: + +```json +{ + "9998BASE11": { + "status": "pending_import", + "last_batch": "2025-01-01T12:55:00" + }, + "1234BASE05": { + "status": "pending_import", + "last_batch": "2025-01-01T12:55:00" + } +} +``` + +This is used for tracking what was included in which batch. + +### SMB / Windows Share Upload + +The script uploads `clients.xml` to an SMB share using the `pysmb` library. + +Key config: + +```python +SMB_ENABLED = True +SMB_SERVER_IP = "10.181.149.83" +SMB_SERVER_NAME = "PATRIOT" +SMB_SHARE_NAME = "api_import" + +SMB_USERNAME = "administrator" +SMB_PASSWORD = "...." +SMB_DOMAIN = "WORKGROUP" + +SMB_REMOTE_PATH = "clients.xml" +SMB_RESULTS_FILENAME = "clients_Import_Results.txt" +``` + +Upload behavior: + +1. **Optional pre-upload check**: + - Attempts to download existing `clients.xml` and `clients_Import_Results.txt` from the share. + - Parses all `<__id>` values from the remote `clients.xml`. + - For each `__id`, checks whether `clients_Import_Results.txt` contains a line with that id and the phrase `"Completed processing client"`. + - Any ids missing a success line are logged into `import_errors.log` with timestamp. + +2. **Rotation of existing clients.xml**: + - If `clients.xml` already exists on the share, it is renamed with a timestamp, e.g.: + `clients_20250101_120000.xml` + +3. **Upload new clients.xml**: + - The new combined XML is uploaded as `clients.xml`. + +4. **Local cleanup**: + - If upload succeeds, the per-client XMLs that were included in this batch are deleted from `out/clients/...`. + - If upload fails, per-client XMLs are **not** deleted, so the batch can be retried later. + +### Combining Logic + +- The script looks for all `*.xml` files under `XML_ROOT_PATH` (recursively). +- It parses each file and extracts `` elements with a `<__id>` child. +- Rows are keyed by `__id`; if the same `__id` appears multiple times, the **last one wins** in the combined XML. +- All rows are appended under a single root `` element. +- A limit `MAX_CLIENTS_PER_RUN` can be set (default 300). + +### Scheduling + +The script is written to run in an infinite loop: + +```python +if __name__ == "__main__": + main() +``` + +Where `main()` essentially: + +1. Calls `sleep_until_next_run()` β†’ sleeps until ~hh:59 before the next hour. +2. Runs `combine_xml_once()`. +3. Sleeps 1 second as a safety fallback. +4. Repeats. + +You can run it as a **systemd service** (recommended) or screen/tmux process. + +Example systemd unit (`/etc/systemd/system/patriot-xml-combine.service`): + +```ini +[Unit] +Description=Patriot XML Combiner & SMB Uploader +After=network-online.target + +[Service] +Type=simple +User=patriot +WorkingDirectory=/opt/patriot_api +ExecStart=/usr/bin/python3 /opt/patriot_api/combine_xml.py +Restart=always +RestartSec=10 + +[Install] +WantedBy=multi-user.target +``` + +Then: + +```bash +sudo systemctl daemon-reload +sudo systemctl enable patriot-xml-combine.service +sudo systemctl start patriot-xml-combine.service +``` + +Logs will be available both in: + +- `/opt/patriot_api/xml_combine.log` +- `journalctl -u patriot-xml-combine.service -f` + +--- + +# βœ… Summary + +- The REST API manages **clients, zones, and users**, per API key. +- Each API key has **fully isolated data** and a dedicated XML output folder. +- `<__id>` in XML is generated as `client_id + port` from `keys.json`. +- Extra XML-only fields (installer info, callouts, client groupings, User 199) are injected per key, not via the API. +- A companion script periodically **combines per-client XMLs** into one `clients.xml`, uploads it to a Windows SMB share, and tracks client import state. + +This README describes all the endpoints and the companion XML pipeline required for integrating with Patriot. \ No newline at end of file diff --git a/xml_combine_for_import.py b/xml_combine_for_import.py index 13a7031..d435853 100755 --- a/xml_combine_for_import.py +++ b/xml_combine_for_import.py @@ -5,7 +5,6 @@ import logging import os import copy import time -import shutil import socket from datetime import datetime, timedelta from io import BytesIO