# Patriot API The **Alarm24 Patriot API** is a REST API used to manage clients, zones, and users in the format required by Patriot. --- ## 🔐 Authentication Every request must include a authorization header: ```http Authorization: Bearer ``` If the key is missing, invalid, disabled, or expired → **401 Unauthorized**. --- ## 📘 Common Status Codes | 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 endpoints require IDs in HTTP headers: | Header | Purpose | |--------|---------| | **X-Client-Id** | Selects the client | | **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 (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 ``` **Response (example):** ```json { "client_id": 9998, "info": { ... }, "zones": { ... }, "users": { ... } } ``` --- ### PUT `/clients` Create or update a client (**UPSERT**). - Returns **201 Created** on first creation. - Returns **200 OK** on subsequent updates. **Body (all supported fields):** ```json { "client_id": 9998, "info": { "Name": "Ola Nordmann", "Alias": "", "Location": "Produksjonsveien 13", "area_code": "1604", "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": 30, "ResetNosigsIgnored": true, "ResetNosigsDays": 7, "ResetNosigsHrs": 0, "ResetNosigsMins": 0, "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 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": 9998, "info": { "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 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": 9998 } ``` Response: **204 No Content** on success. --- ## 🔹 ZONES ### GET `/clients/zones` List all zones for a client. **Headers:** ```http X-Client-Id: 9998 Authorization: Bearer ``` **Response example:** ```json { "client_id": 9998, "zones": [ { "Zone_No": 1, "Zone_area": "Inngang", "ModuleNo": 0 }, { "Zone_No": 2, "Zone_area": "Stue", "ModuleNo": 0 } ] } ``` --- ### POST `/clients/zones/list` Alternative way to list zones using JSON body. **Body:** ```json { "client_id": 9998 } ``` --- ### PUT `/clients/zones` Create or update (UPSERT) a zone. **Body:** ```json { "client_id": 9998, "zone_id": 1, "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 specific zone for a client. **Body:** ```json { "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 } ``` --- ### POST `/clients/users` Create a new user. **Body:** ```json { "client_id": 9998, "user": { "User_Name": "User Number 1", "MobileNo": "+4712345678", "MobileNoOrder": 1, "Email": "user@email.com", "Type": "U", "UserNo": 1, "Instructions": "Optional", "CallOrder": 0 } } ``` If `UserNo` already exists → **409 Conflict**. --- ### POST `/clients/users/get` Get one specific user using JSON body. **Body:** ```json { "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", "MobileNoOrder": 1, "Email": "new@email.com", "Type": "U", "UserNo": 1, "Instructions": "New instructions", "CallOrder": 0 } } ``` User is identified by `client_id` + `UserNo`. --- ### DELETE `/clients/users` Delete a specific user. **Body:** ```json { "client_id": 9998, "user_no": 1 } ``` Response: **204 No Content** on success. --- 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.