736 lines
15 KiB
Markdown
736 lines
15 KiB
Markdown
# 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 <API_KEY>
|
|
```
|
|
|
|
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/<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
|
|
|
|
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
|
|
|
|
### 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 <API_KEY>
|
|
```
|
|
|
|
**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 <API_KEY>
|
|
```
|
|
|
|
**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 <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`
|
|
|
|
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/**/<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. |