v.0.9 Beta

This commit is contained in:
Anders Knutsen 2025-12-03 09:51:31 +01:00
parent c9a3292bdd
commit 2b0972a22c
2 changed files with 596 additions and 123 deletions

712
README.md
View File

@ -1,199 +1,444 @@
# Patriot API # 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 ## 🔐 Authentication
All requests require: Every request must include a authorization header:
``` ```http
Authorization: Bearer <API_KEY> Authorization: Bearer <API_KEY>
``` ```
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 ## 📘 Common Status Codes
| Status | Meaning | | Status | Meaning |
|--------|---------| |--------|------------------------------------------------|
| **200 OK** | Request successful (read or update) | | **200 OK** | Request successful |
| **201 Created** | Resource successfully created | | **201 Created** | Resource created |
| **204 No Content** | Resource deleted | | **204 No Content** | Resource deleted |
| **401 Unauthorized** | Invalid/missing API key | | **401 Unauthorized** | Missing/invalid/disabled/expired API key |
| **404 Not Found** | Client, zone, or user not found | | **404 Not Found** | Client/zone/user not found |
| **409 Conflict** | Duplicate user number | | **409 Conflict** | Duplicate record, or client temporarily locked |
| **422 Unprocessable Entity** | Validation failed | | **422 Unprocessable Entity** | Validation failed, check request format |
--- ---
## 🧩 Required Headers ## 🧩 Required Headers
Some GET requests require client/user IDs in headers: Some GET endpoints require IDs in HTTP headers:
| Header | Purpose | | Header | Purpose |
|--------|---------| |--------|---------|
| **X-Client-Id** | Selects the client | | **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/<key_name>/<client_id>.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</__id>
```
- `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>Installer Name</Installer>
<UseGlobCallOuts>True</UseGlobCallOuts>
<ShowOnCallOuts>False</ShowOnCallOuts>
<GlobCallOuts>TLF01BASE01</GlobCallOuts>
<UseGlobCallOuts2>True</UseGlobCallOuts2>
<ShowOnCallOuts2>False</ShowOnCallOuts2>
<GlobCallOuts2>TLF02BASE01</GlobCallOuts2>
<AltLookup>True</AltLookup>
<AltAlarmNo>CID4BASE01</AltAlarmNo>
<ConvertType>None</ConvertType>
<SIGINTERPRET>SIADecimal</SIGINTERPRET>
<ClientGroupings>
<ClientGrouping>
<Description>Alarm24</Description>
<GroupingTypeDescription>Alarm24 Tilgang</GroupingTypeDescription>
<GroupingAllowMultiple>True</GroupingAllowMultiple>
</ClientGrouping>
<ClientGrouping>
<Description>Import</Description>
<GroupingTypeDescription>Østfold Sikkerhetsservice AS</GroupingTypeDescription>
<GroupingAllowMultiple>True</GroupingAllowMultiple>
</ClientGrouping>
</ClientGroupings>
```
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
<User>
<User_Name>Installer Name</User_Name>
<Email>installer@email.com</Email>
<UserNo>199</UserNo>
<CallOrder>0</CallOrder>
<Type>N</Type>
</User>
```
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 # 📂 API Endpoints
## ======================= Below are the available endpoints and request/response formats.
> **Note:** All examples assume you are also sending the correct `Authorization: Bearer <API_KEY>` header.
---
## 🔹 CLIENTS ## 🔹 CLIENTS
## =======================
## **GET /clients** ### GET `/clients`
Get full client info.
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 <API_KEY>
``` ```
X-Client-Id: <client_id>
**Response (example):**
```json
{
"client_id": 9998,
"info": { ... },
"zones": { ... },
"users": { ... }
}
``` ```
--- ---
## **PUT /clients** ### PUT `/clients`
Create or update a client (UPSERT).
Returns: Create or update a client (**UPSERT**).
- `201 Created` if new
- `200 OK` if updated
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": { "info": {
"Name": "Customer Name", "Name": "Ola Nordmann",
"Location": "Address line 1", "Alias": "",
"Location": "Produksjonsveien 13",
"area_code": "1604", "area_code": "1604",
"area": "City", "area": "Fredrikstad",
"AltLookup": true,
"AltAlarmNo": "SIA1000101", "BusPhone": "69310000",
"ConvertType": "None", "Email": "post@ostsik.no",
"NoSigsMon": "None", "OKPassword": "franzjager",
"SinceDays": 0, "SpecRequest": "Dette skal gjøres ved alarm på denne kunden.",
"NoSigsMon": 1,
"SinceDays": 1,
"SinceHrs": 0, "SinceHrs": 0,
"SinceMins": 0, "SinceMins": 30,
"ResetNosigsIgnored": true, "ResetNosigsIgnored": true,
"ResetNosigsDays": 0, "ResetNosigsDays": 7,
"ResetNosigsHrs": 0, "ResetNosigsHrs": 0,
"ResetNosigsMins": 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** ### PATCH `/clients`
Partial update of a client.
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": { "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 `/clients`
Delete entire client and its XML file.
``` 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. List all zones for a client.
Requires: **Headers:**
```http
X-Client-Id: 9998
Authorization: Bearer <API_KEY>
``` ```
X-Client-Id: <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** ### POST `/clients/zones/list`
Create or update a zone.
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_id": 1,
"Zone_area": "Entrance", "Zone_area": "Inngang",
"ModuleNo": 0 "ModuleNo": 0
} }
``` ```
- `zone_id` must be between **1** and **999**.
- Returns **201 Created** for both create and update (by design).
--- ---
## **DELETE /clients/zones** ### DELETE `/clients/zones`
Delete a zone.
``` Delete a specific zone for a client.
**Body:**
```json
{ {
"client_id": 1234, "client_id": 9998,
"zone_id": 1 "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 <API_KEY>
```
**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 <API_KEY>
```
---
### POST `/clients/users/list`
List all users via JSON body.
**Body:**
```json
{
"client_id": 9998
}
```
--- ---
# ======================= ### POST `/clients/users`
# 🔹 USERS
# =======================
## **GET /clients/users**
List all users for a client.
Requires:
```
X-Client-Id: <client_id>
```
---
## **GET /clients/users/single**
Get one user.
Headers required:
```
X-Client-Id: <client_id>
X-User-No: <user_no>
```
---
## **POST /clients/users**
Create a new user. Create a new user.
``` **Body:**
```json
{ {
"client_id": 1234, "client_id": 9998,
"user": { "user": {
"User_Name": "User Number 1", "User_Name": "User Number 1",
"MobileNo": "+4712345678", "MobileNo": "+4712345678",
@ -207,14 +452,34 @@ Create a new user.
} }
``` ```
If `UserNo` already exists → **409 Conflict**.
--- ---
## **PUT /clients/users** ### POST `/clients/users/get`
Update a user (full replace).
``` 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": {
"User_Name": "Changed Name", "User_Name": "Changed Name",
"MobileNo": "+4798765432", "MobileNo": "+4798765432",
@ -222,41 +487,250 @@ Update a user (full replace).
"Email": "new@email.com", "Email": "new@email.com",
"Type": "U", "Type": "U",
"UserNo": 1, "UserNo": 1,
"Instructions": "Updated", "Instructions": "New instructions",
"CallOrder": 0 "CallOrder": 0
} }
} }
``` ```
User is identified by `client_id` + `UserNo`.
--- ---
## **DELETE /clients/users** ### DELETE `/clients/users`
Delete a user.
``` Delete a specific user.
**Body:**
```json
{ {
"client_id": 1234, "client_id": 9998,
"user_no": 1 "user_no": 1
} }
``` ```
--- Response: **204 No Content** on success.
# 🚀 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
--- ---
# 📄 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/**/<client_id>.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 `<Row>` 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 `<Clients>` 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.

View File

@ -5,7 +5,6 @@ import logging
import os import os
import copy import copy
import time import time
import shutil
import socket import socket
from datetime import datetime, timedelta from datetime import datetime, timedelta
from io import BytesIO from io import BytesIO