Salto Encoder API — Key-Identifikation, User-Config & Batch-Encode
Stand: 20.03.2026
Architektur
- Encoder (USB) → Windows VM (172.16.41.129) → Local IO Bridge (localhost:50000) → WebSocket → SaltoServer (10.128.40.6:8102)
- API-Aufrufe gehen an SaltoServer (10.128.40.6:8100)
- Bridge-Pipe muss VOR dem API-Call geöffnet werden und darf nicht ablaufen
Auth (Ptlc-Mode — KRITISCH)
ProAccess Space nutzt "Ptlc"-Mode (case-insensitive Passwort). Der Client muss zwei Hashes senden:
M = salt1 + sha256(salt1 + password)
if password != password.lower():
M += salt2 + sha256(salt2 + password.lower())
- Salt: 16x 4 hex chars = 64 hex chars (JS:
Math.floor(65536*(1+Math.random())).toString(16).substring(1)) - Username: base64(encodeURIComponent(username))
- Credentials:
anknor/Zaphod42! - Endpoint:
POST /oauth/connect/token(grant_type=password, client_id=webapp, scope=offline_access global) - 5 Fehlversuche = Lockout → Reset:
UPDATE tb_Operators SET LockoutCounter=0 WHERE log_username='anknor'
Bridge-Pipe öffnen (bewährter Befehl)
sshpass -p 'zaphod42' ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no ak@172.16.41.129 \
'powershell -NonInteractive -Command "$body = \"{`\"token`\":`\"MEIN_TOKEN`\",`\"server`\":`\"ws://10.128.40.6:8102/localiobridge`\",`\"timeout`\":120}\"; $r = Invoke-WebRequest -Uri http://localhost:50000/ports/LNDYBJ0/pipe -Method POST -Body $body -ContentType application/json -UseBasicParsing; Write-Output $r.StatusCode"'
- Muss
200und danachpipe: truebei GET /ports/LNDYBJ0 zurückgeben - Escaping ist kritisch: single-quotes um SSH-Befehl, PowerShell backticks für JSON-Anführungszeichen
- Pipe-Timeout: max 120s, danach verfällt sie
StartReadKey API-Call
POST /rpc/StartReadKey
{
"targetDevice": {
"$type": "Salto.Services.Web.Model.Dto.Devices.Common.StreamedTargetEncoder",
"DeviceType": 0,
"Token": "MEIN_TOKEN", // gleicher Token wie bei Pipe!
"BeeperEnabled": false,
"Timezone": "W. Europe Standard Time",
"Id": "LNDYBJ0"
},
"timeout": 60, // MAX 60!
"waitTilKeyRemoved": false
}
- Gibt eine UID (String) zurück
- timeout max 60 (Server lehnt >60 ab)
- Pipe muss offen sein, sonst "Peripheral not initialized" (607)
- Sofort nach Pipe-Öffnung aufrufen — je weniger Zeitverzug, desto besser
GetStatusOfReadKey Poll
POST /rpc/GetStatusOfReadKey
{ "uid": "die-uid-von-startreadkey" }
- Pollen bis
IsFinished: true - Ergebnis in
Outcome: UserId.Name/UserId.Id— PersonRomCode— physische NFC-UIDKeyIsValid— GültigkeitExpirationExists+Expiration— AblaufdatumKeyAccessPermissionsList[].AccessObjectList[].Id.Name— Zonen- ErrorCode 511 = Timeout (keine Karte erkannt)
- ErrorCode 607 = Peripheral not initialized (Pipe fehlt/abgelaufen)
StartUpdateKeyToUser — Key enkodieren
POST /rpc/StartUpdateKeyToUser
{
"userId": 105,
"targetDevice": {
"$type": "Salto.Services.Web.Model.Dto.Devices.Common.StreamedTargetEncoder",
"DeviceType": 0,
"Token": "MEIN_TOKEN",
"BeeperEnabled": false,
"Timezone": "W. Europe Standard Time",
"Id": "LNDYBJ0"
},
"timeout": 60
}
- Gibt UID zurück, dann pollen mit
GetStatusOfUpdateKeyToUser({uid: "..."}) - Pipe muss offen sein (gleich wie bei Read)
- ErrorCode 510 = "Invalid user key" (Karte schon aktuell oder gehört nicht zum User)
- ErrorCode 511 = Timeout
- ErrorCode 607 = Peripheral not initialized (Pipe fehlt)
Chip-Nummer (physisch aufgedruckt)
Die auf Salto-Karten/Keys aufgedruckte Nummer ("Chip Nummer" in der ProAccess UI) ist in tb_Users.Dummy1 (nvarchar) gespeichert.
- Für die Zusammenarbeit immer die Chip-Nummer (Dummy1) verwenden — einzige Nummer, die physisch auf der Karte lesbar ist
- Format: 4-stellig mit führender Null (z.B. "0132" → DB-Wert "132"), oder 6-stellig (z.B. "104355")
- NICHT identisch mit Cardcode, Codkey oder ROMCode
- Bei Abfragen und Listen IMMER
u.Dummy1 as ChipNrmit ausgeben
Feld-Zuordnung
| UI-Label | DB-Feld | Beispiel |
|---|---|---|
| Chip Nummer (aufgedruckt) | tb_Users.Dummy1 | 132, 104355 |
| Cardcode (intern) | tb_Cards.Cardcode | 383 |
| Codkey (intern) | tb_Cards.Codkey | 253 |
| ROM Code (NFC UID) | tb_Cards.ROMCode | 047B74B2754480 |
Beispiel: User per Chip-Nummer finden
SELECT c.Cardcode, u.name, u.Dummy1 as ChipNr, c.ROMCode, u.UpdatePeriod,
CONVERT(varchar, c.ExpirationDate, 104) as Ablauf
FROM tb_Cards c
JOIN tb_Users u ON c.id_user = u.id_user
WHERE u.Dummy1 = '132' -- Suche nach Chip-Nr 0132
User-Konfiguration via DB (tb_Users)
User-Einstellungen wie Ablaufdaten und Update-Zeiträume können nur über direkte DB-Änderungen gesetzt werden (API-Einschränkungen, siehe unten).
Relevante Felder
| Feld | Typ | Beschreibung |
|---|---|---|
Dummy1 |
nvarchar | Chip-Nummer (physisch aufgedruckt, siehe oben) |
WithExpiration |
bit | Häkchen "Personenablauf" in ProAccess UI |
dtExpiration |
datetime | Ablaufdatum User-Ebene |
KeyExpDiffersFromUserExp |
bit | Key-Ablauf eigenständig vom User |
UpdatePeriod |
int | Erneuerungszeitraum in Tagen |
UpdatePeriodType |
tinyint | Typ des Erneuerungszeitraums |
UseLockCalendar |
bit | Mediengültigkeit-Erneuerung |
EditVersion |
int | Bei jeder Änderung hochzählen |
EditLoaded |
bit | KRITISCH: = 1 setzt "Update erforderlich" in ProAccess UI |
Beispiel-SQL für User-Update
SET QUOTED_IDENTIFIER ON;
UPDATE tb_Users SET
WithExpiration = 1,
dtExpiration = CONVERT(datetime, '2026-09-20T23:59:59', 126),
KeyExpDiffersFromUserExp = 1,
UpdatePeriod = 180,
UpdatePeriodType = 0,
UseLockCalendar = 1,
EditVersion = EditVersion + 1,
EditLoaded = 1
WHERE Id = 105;
Hinweise
SET QUOTED_IDENTIFIER ON;vor jedem UPDATE (wegen indizierter Views)- Datetime-Werte mit
CONVERT(datetime, '2026-09-20T23:59:59', 126)(ISO-Format) EditLoaded = 1ist der Schlüssel damit ProAccess "Update erforderlich" anzeigt- Referenz-User: Hansen Luisa (User 104, Card 1714) — funktionierendes Setup mit KeyExpDiffersFromUserExp=1, UpdatePeriod=180, ExpirationDate auf Card-Ebene
Card-Konfiguration via DB (tb_Cards)
| Feld | Typ | Beschreibung |
|---|---|---|
ExpirationDate |
datetime | Ablaufdatum auf der physischen Karte |
NewCount |
int | Edition-Counter |
BinaryAccess |
varbinary | Binäre Kartendaten, NICHT manuell ändern — wird beim Enkodieren neu berechnet |
Beispiel-SQL für Card-Update
SET QUOTED_IDENTIFIER ON;
UPDATE tb_Cards SET
ExpirationDate = CONVERT(datetime, '2026-09-20T23:59:59', 126)
WHERE Id = 1714;
Hinweise
- ExpirationDate-Format:
'2026-09-20T23:59:59'(ISO, wie bei Hansen Karte 1714) - BinaryAccess NICHT auf NULL setzen oder manuell ändern
Batch-Workflow für Key-Updates
1. DB-Felder setzen
Für jeden betroffenen User und dessen Card die oben beschriebenen Felder per SQL setzen: - User: WithExpiration, dtExpiration, UpdatePeriod, KeyExpDiffersFromUserExp, EditLoaded=1, EditVersion+1 - Card: ExpirationDate
2. ProAccess Service ggf. neustarten
Falls Änderungen nicht sofort sichtbar sind (siehe ProAccess Service-Management unten).
3. Encode-Loop pro Karte
Pipe öffnen → StartReadKey (User-ID ermitteln) → Pipe schließen
→ neue Pipe → StartUpdateKeyToUser(userId) → Pipe schließen
- Zwei Pipes nötig: eine zum Lesen (Identifikation), eine zum Schreiben (Update)
- Script:
/tmp/salto_encode_loop.py(sollte nach~/scripts/verschoben werden)
ProAccess Service-Management
ProAccessSpaceService.exeist KEIN Windows-Service, sondern wird vomProAccessSpaceConfigurator.exe(Tray-App) gestartetStop-Process -Name ProAccessSpaceServicekillt den Service- Neustart NUR über den Configurator möglich (braucht RDP/Desktop-Session) — NICHT per SSH startbar
Get-Process *ProAccess*zeigt beide Prozesse- Port 8100 = Web-Interface/API
- Nach DB-Änderungen ggf. Service neustarten damit ProAccess die neuen Werte liest
Bekannte API-Einschränkungen
| API-Methode | Status | Problem |
|---|---|---|
GetUserById |
kaputt | Gibt immer "Invalid input parameter" (detail=5) mit userId als int, oder "not found" (detail=1) mit IdAndName. Vermutlich Salto-Version-Bug. |
GetUserSummaryByIdForBadging |
funktioniert | {"userId": 86} (int) → gibt UserSummary (Id, Name, Partition), aber kein vollständiges User-Objekt |
UpdateUser |
nicht nutzbar | Braucht vollständiges User-Objekt, das nur via GetUserById zu bekommen wäre |
Konsequenz: User-Konfiguration NUR über direkte DB-Änderungen möglich, nicht über die API.
Encoder Port-ID
- Bridge API:
GET http://localhost:50000/ports→device_type: 0= Encoder - Aktuell: ID
LNDYBJ0, Serial040000036043
Script-Standorte
| Script | Standort | Funktion | Lauffähig auf |
|---|---|---|---|
salto_read_keys.py |
~/scripts/ (Mac + Pi) |
Key-Identifikation mit Chip-Nr | NUR Mac |
salto_encode_keys.py |
~/scripts/ (Mac + Pi) |
Batch-Encode (Read+Update pro Karte) | NUR Mac |
salto_config_users.py |
~/scripts/ (Mac + Pi) |
User-DB-Konfiguration (Ablauf, Period) | Mac + Pi |
salto_api.py |
~/scripts/ (nur raspip5) |
Allgemeiner API-Client (Auth-Bug!) | Pi |
Skripte
Alle Skripte liegen in ~/scripts/ auf Mac und Pi. Die Encoder-Skripte (read, encode) funktionieren nur vom Mac, da die Windows VM (172.16.41.129) per VMware NAT nur vom Mac erreichbar ist. Das Config-Skript funktioniert auch vom Pi (nur SSH zum SaltoServer).
salto_read_keys.py — Key-Identifikation
Liest Keys/Armbänder nacheinander auf dem Encoder und identifiziert sie. Zeigt Name, Chip-Nummer, Gültigkeit, Ablauf und Zonen. Duplikate werden erkannt.
python3 ~/scripts/salto_read_keys.py # 3 Min Laufzeit (Default)
python3 ~/scripts/salto_read_keys.py -t 5 # 5 Minuten
python3 ~/scripts/salto_read_keys.py -t 0 # Unbegrenzt (nur Ctrl+C)
salto_encode_keys.py — Batch-Encode
Pro Karte: Read (User-ID ermitteln) → neue Pipe → Update (Key encodieren). Duplikate werden übersprungen, Error 510 (bereits aktuell) wird als SKIP gewertet.
python3 ~/scripts/salto_encode_keys.py # Alle Karten
python3 ~/scripts/salto_encode_keys.py -u 104 105 106 # Nur bestimmte User-IDs
python3 ~/scripts/salto_encode_keys.py -c 132 0145 # Nur bestimmte Chip-Nummern
python3 ~/scripts/salto_encode_keys.py --pending # Nur User mit EditLoaded=1
python3 ~/scripts/salto_encode_keys.py -t 10 # 10 Min Timeout
salto_config_users.py — User-Konfiguration via DB
Setzt WithExpiration, KeyExpDiffersFromUserExp, UpdatePeriod, dtExpiration, Card ExpirationDate, EditLoaded=1 und EditVersion+1 direkt per SQL. Zeigt Vorher/Nachher-Vergleich.
python3 ~/scripts/salto_config_users.py 104 105 106 # User-IDs
python3 ~/scripts/salto_config_users.py -c 132 0145 104355 # Chip-Nummern
python3 ~/scripts/salto_config_users.py --all-active # Alle aktiven User
python3 ~/scripts/salto_config_users.py 104 --period 90 # 90 Tage Erneuerung
python3 ~/scripts/salto_config_users.py 104 --months 12 # 12 Monate Ablauf
python3 ~/scripts/salto_config_users.py 104 --dry-run # Nur anzeigen
Typischer Workflow
- Config:
python3 salto_config_users.py --all-active(DB-Felder setzen) - Encode:
python3 salto_encode_keys.py --pending -t 30(alle Keys mit "Update erforderlich" encodieren) - Verify:
python3 salto_read_keys.py -t 5(Stichproben lesen)
Bekannte Probleme
salto_api.pyauf raspip5 hat den alten Auth-Code OHNE Ptlc → muss gefixt werden- Pipe-Timing: zwischen Pipe-Open und StartReadKey darf wenig Zeit vergehen
- PowerShell-Escaping über SSH ist extrem fragil