The lock in this WG is a Homematic IP door lock (keypad + alarm), not Nuki/Tuya as originally specced — confirmed with the user. Homematic IP has no official self-service partner API for Access-Point-only setups (no local CCU), so this uses the actively-maintained open-source `homematicip` Python library (implements the same reverse-engineered cloud protocol used by the official app) via two scripts rather than reimplementing the HTTP/HMAC handshake from scratch — lower risk of subtly wrong protocol details. - scripts/hmip_register.py: non-interactive pairing (connection request -> wait for the physical blue-button press on the Access Point -> auth token). Writes progress to a status file so the API can poll it instead of blocking a request for up to ~6 minutes. - scripts/hmip_control.py: list-devices / lock / unlock via HMIP_ACCESS_POINT + HMIP_AUTH_TOKEN env vars (not CLI args, so they don't leak into process listings). - routes/smartLock.ts: POST /smartlock/pairing/start + GET .../status (LANDLORD/ADMIN, one-time setup), GET /smartlock/devices (LANDLORD/ADMIN), POST /smartlock/devices/:id/:lock|unlock (any authenticated user — this is the tenant-facing "keyless door" use case from the spec). All lock/unlock routes 409 until pairing has produced HMIP_ACCESS_POINT/HMIP_AUTH_TOKEN. Does NOT cover temporary guest PIN codes on the keypad itself — that's handled through Homematic IP's own "eSchlüssel" app feature, which isn't exposed by this API; guest codes remain DB-only as before.
73 lines
2.2 KiB
Python
73 lines
2.2 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Steuerungs-Skript für das Homematic-IP-Türschloss.
|
|
Nutzt AUTH_TOKEN/ACCESS_POINT aus Umgebungsvariablen (nicht als CLI-Argument,
|
|
damit sie nicht in Prozesslisten/Logs auftauchen).
|
|
|
|
Usage:
|
|
python3 hmip_control.py list-devices
|
|
python3 hmip_control.py lock <deviceId>
|
|
python3 hmip_control.py unlock <deviceId>
|
|
|
|
Gibt bei list-devices JSON-Liste aller DoorLockDrive-Geräte aus
|
|
({"id": ..., "label": ..., "lockState": ...}), sonst {"ok": true}.
|
|
"""
|
|
import asyncio
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
import homematicip
|
|
from homematicip.base.enums import LockState
|
|
from homematicip.connection.connection_context import ConnectionContextBuilder
|
|
from homematicip.home import Home
|
|
|
|
|
|
async def get_home():
|
|
access_point = os.environ["HMIP_ACCESS_POINT"]
|
|
auth_token = os.environ["HMIP_AUTH_TOKEN"]
|
|
context = await ConnectionContextBuilder.build_context_async(access_point, auth_token=auth_token)
|
|
home = Home()
|
|
await home.init_async(access_point, auth_token=auth_token)
|
|
await home.get_current_state_async()
|
|
return home
|
|
|
|
|
|
async def main():
|
|
if len(sys.argv) < 2:
|
|
print(json.dumps({"error": "Kommando fehlt (list-devices|lock|unlock)"}))
|
|
sys.exit(1)
|
|
|
|
command = sys.argv[1]
|
|
home = await get_home()
|
|
|
|
door_locks = [d for d in home.devices if type(d).__name__.startswith("DoorLockDrive")]
|
|
|
|
if command == "list-devices":
|
|
print(json.dumps([
|
|
{"id": d.id, "label": d.label, "lockState": str(d.lockState)}
|
|
for d in door_locks
|
|
]))
|
|
return
|
|
|
|
if command in ("lock", "unlock"):
|
|
if len(sys.argv) != 3:
|
|
print(json.dumps({"error": "deviceId fehlt"}))
|
|
sys.exit(1)
|
|
device_id = sys.argv[2]
|
|
target = next((d for d in door_locks if d.id == device_id), None)
|
|
if not target:
|
|
print(json.dumps({"error": f"Türschloss {device_id} nicht gefunden"}))
|
|
sys.exit(1)
|
|
state = LockState.LOCKED if command == "lock" else LockState.UNLOCKED
|
|
await target.set_lock_state_async(state)
|
|
print(json.dumps({"ok": True}))
|
|
return
|
|
|
|
print(json.dumps({"error": f"Unbekanntes Kommando: {command}"}))
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|