Zum Inhalt

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 200 und danach pipe: true bei 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 — Person
  • RomCode — physische NFC-UID
  • KeyIsValid — Gültigkeit
  • ExpirationExists + Expiration — Ablaufdatum
  • KeyAccessPermissionsList[].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 ChipNr mit 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 = 1 ist 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.exe ist KEIN Windows-Service, sondern wird vom ProAccessSpaceConfigurator.exe (Tray-App) gestartet
  • Stop-Process -Name ProAccessSpaceService killt 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/portsdevice_type: 0 = Encoder
  • Aktuell: ID LNDYBJ0, Serial 040000036043

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

  1. Config: python3 salto_config_users.py --all-active (DB-Felder setzen)
  2. Encode: python3 salto_encode_keys.py --pending -t 30 (alle Keys mit "Update erforderlich" encodieren)
  3. Verify: python3 salto_read_keys.py -t 5 (Stichproben lesen)

Bekannte Probleme

  • salto_api.py auf 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