253 lines
7.3 KiB
Markdown
253 lines
7.3 KiB
Markdown
# NFC Door Plan: Kwikset Home Connect 918 + Zooz ZST39 LR
|
|
|
|
This plan is for adding L484 member-card access to the existing **Kwikset Home Connect 918** lock.
|
|
|
|
The Kwikset 918 is a Z-Wave lock. It does not read NFC cards directly, so the NFC reader is only the credential scanner. The actual unlock command must be sent through a Z-Wave controller.
|
|
|
|
## Hardware
|
|
|
|
- Lock: Kwikset Home Connect 918.
|
|
- NFC reader: PN532 NFC V3 Module Reader/Writer board.
|
|
- Reader controller: ESP32 dev board.
|
|
- Z-Wave coordinator: Zooz 800 Series Z-Wave Long Range S2 USB Stick ZST39 LR.
|
|
- On-prem host: a local server running the L484 backend plus either Home Assistant or Z-Wave JS.
|
|
|
|
## Architecture
|
|
|
|
```text
|
|
Member NFC card
|
|
-> PN532 reader
|
|
-> ESP32 over I2C/SPI
|
|
-> L484 backend /api/access/check
|
|
-> allow/deny decision and access log
|
|
-> local Z-Wave bridge
|
|
-> Zooz ZST39 LR
|
|
-> Kwikset Home Connect 918 unlock command
|
|
```
|
|
|
|
The ESP32 should not directly unlock the Kwikset. It should only read the card and ask the local backend whether access is allowed.
|
|
|
|
## Local Services
|
|
|
|
Run these on the on-prem server:
|
|
|
|
- L484 backend and admin app.
|
|
- Z-Wave JS or Home Assistant with Z-Wave JS integration.
|
|
- Zooz ZST39 LR USB stick paired to the Kwikset 918.
|
|
|
|
The simplest operational setup is Home Assistant:
|
|
|
|
```text
|
|
ESP32 -> L484 API -> Home Assistant webhook/service call -> lock.unlock
|
|
```
|
|
|
|
Alternative setup:
|
|
|
|
```text
|
|
ESP32 -> L484 API -> Z-Wave JS API/MQTT -> Kwikset unlock
|
|
```
|
|
|
|
Home Assistant is likely easier to operate and inspect.
|
|
|
|
## Access Flow
|
|
|
|
1. Member taps NFC card on the PN532 reader.
|
|
2. ESP32 reads the card credential.
|
|
3. ESP32 sends the credential to the local L484 backend.
|
|
4. Backend checks:
|
|
- card exists,
|
|
- card is active,
|
|
- member exists,
|
|
- member is paid/active,
|
|
- member is not suspended/revoked/expired.
|
|
5. Backend records an access log.
|
|
6. If allowed, the backend calls the local Home Assistant webhook.
|
|
7. Home Assistant/Z-Wave JS sends unlock to the Kwikset 918 through the Zooz stick.
|
|
|
|
## Controller API
|
|
|
|
The ESP32 calls:
|
|
|
|
```http
|
|
POST /api/access/check
|
|
X-Controller-Token: <ACCESS_CONTROLLER_TOKEN>
|
|
Content-Type: application/json
|
|
|
|
{
|
|
"doorId": "front-door",
|
|
"cardCredential": "<secret read from NFC card>"
|
|
}
|
|
```
|
|
|
|
Allowed response:
|
|
|
|
```json
|
|
{
|
|
"allow": true,
|
|
"reason": "active_member_card",
|
|
"member": {
|
|
"membershipId": "L484-2026-XXXXXX",
|
|
"fullName": "Member Name",
|
|
"status": "active"
|
|
}
|
|
}
|
|
```
|
|
|
|
Denied response:
|
|
|
|
```json
|
|
{
|
|
"allow": false,
|
|
"reason": "member_suspended",
|
|
"member": {
|
|
"membershipId": "L484-2026-XXXXXX",
|
|
"fullName": "Member Name",
|
|
"status": "suspended"
|
|
}
|
|
}
|
|
```
|
|
|
|
## PN532 to ESP32
|
|
|
|
Start with I2C.
|
|
|
|
Photo overlay using the supplied board pictures:
|
|
|
|

|
|
|
|
Breadboard layout:
|
|
|
|

|
|
|
|
Clean schematic:
|
|
|
|

|
|
|
|
Typical wiring:
|
|
|
|
- PN532 `VCC` -> ESP32 `3V3`
|
|
- PN532 `GND` -> ESP32 `GND`
|
|
- PN532 `SDA` -> ESP32 GPIO 21
|
|
- PN532 `SCL` -> ESP32 GPIO 22
|
|
- PN532 `IRQ` -> ESP32 GPIO 4
|
|
- PN532 `RSTO/RST` -> ESP32 GPIO 5
|
|
|
|
If reads are unreliable, switch to SPI and keep the cable short.
|
|
|
|
## Card Credential Model
|
|
|
|
Do not rely on raw UID-only cards for production.
|
|
|
|
Preferred first version:
|
|
|
|
- Write a random card secret to a writable NFC card/tag.
|
|
- Register that secret in the L484 admin panel when issuing the card.
|
|
- Backend stores only `HMAC_SHA256(secret, ACCESS_HMAC_KEY)`.
|
|
- ESP32 reads and submits the secret.
|
|
|
|
Raw UID can be retained as a fallback identifier for testing, but it is cloneable and should not be the production access credential.
|
|
|
|
## Z-Wave / Kwikset Setup
|
|
|
|
1. Plug Zooz ZST39 LR into the on-prem server.
|
|
2. Add it to Home Assistant or Z-Wave JS.
|
|
3. Pair the Kwikset Home Connect 918 with S2 security if supported.
|
|
4. Confirm manual `lock.unlock` works from Home Assistant/Z-Wave JS.
|
|
5. Create a local-only webhook/service path that unlocks only after L484 backend approval.
|
|
6. Set `HOME_ASSISTANT_UNLOCK_WEBHOOK_URL` in the L484 backend environment.
|
|
|
|
The unlock call should stay on the LAN. Do not expose Home Assistant or Z-Wave JS publicly for this.
|
|
|
|
## Home Assistant Webhook
|
|
|
|
Use a Home Assistant automation webhook as the backend target. The webhook should call `lock.unlock` for the Kwikset 918 entity.
|
|
|
|
Example automation shape:
|
|
|
|
```yaml
|
|
alias: L484 approved member unlock
|
|
trigger:
|
|
- platform: webhook
|
|
webhook_id: l484-approved-unlock-random-secret
|
|
allowed_methods:
|
|
- POST
|
|
local_only: true
|
|
action:
|
|
- service: lock.unlock
|
|
target:
|
|
entity_id: lock.kwikset_918
|
|
mode: single
|
|
```
|
|
|
|
Set the L484 backend value to the local webhook URL:
|
|
|
|
```bash
|
|
HOME_ASSISTANT_UNLOCK_WEBHOOK_URL=http://homeassistant.local:8123/api/webhook/l484-approved-unlock-random-secret
|
|
```
|
|
|
|
The backend posts member/card metadata to the webhook body for future logging or notifications.
|
|
|
|
## Security Rules
|
|
|
|
- Keep ESP32, power conversion, and wiring inside.
|
|
- Mount only the PN532 reader outside, or protect the wiring if the reader must be outside.
|
|
- Use HTTPS or a trusted isolated LAN between ESP32 and backend where practical.
|
|
- Use a strong `ACCESS_CONTROLLER_TOKEN`.
|
|
- Use a strong `ACCESS_HMAC_KEY`.
|
|
- Fail locked if backend, Wi-Fi, Home Assistant, or Z-Wave is unavailable.
|
|
- Log every allow/deny attempt.
|
|
- Do not store raw card secrets in backend data.
|
|
- Do not put Nostr nsec keys on cards.
|
|
|
|
## Failure Behavior
|
|
|
|
- If card read fails: deny.
|
|
- If backend request times out: deny.
|
|
- If backend says deny: do not call Z-Wave.
|
|
- If Z-Wave unlock fails: log locally and deny/retry once only.
|
|
- If internet is down but LAN is up: system should still work because all services are local.
|
|
|
|
## Development Access Tests
|
|
|
|
When `DEV_SEED_MEMBERS=true`, the backend seeds active and suspended members with matching test card credentials.
|
|
|
|
Example active-card request:
|
|
|
|
```bash
|
|
curl -X POST http://localhost:3001/api/access/check \
|
|
-H "Content-Type: application/json" \
|
|
-H "X-Controller-Token: $ACCESS_CONTROLLER_TOKEN" \
|
|
-d '{"doorId":"front-door","cardCredential":"dev-card-1-L484-2026-DEV07X"}'
|
|
```
|
|
|
|
Expected result: `allow: true`.
|
|
|
|
Example suspended-card request:
|
|
|
|
```bash
|
|
curl -X POST http://localhost:3001/api/access/check \
|
|
-H "Content-Type: application/json" \
|
|
-H "X-Controller-Token: $ACCESS_CONTROLLER_TOKEN" \
|
|
-d '{"doorId":"front-door","cardCredential":"dev-card-4-L484-2026-DEV10X"}'
|
|
```
|
|
|
|
Expected result: `allow: false`.
|
|
|
|
If `HOME_ASSISTANT_UNLOCK_WEBHOOK_URL` is configured, only the active controller request attempts the Home Assistant unlock webhook.
|
|
|
|
## Next Build Tasks
|
|
|
|
Completed in this repo:
|
|
|
|
- ESP32 firmware scaffold with configurable backend URL, controller token, door ID, and PN532 mode.
|
|
- Backend approved-access hook for Home Assistant/Z-Wave unlock.
|
|
- Decision: L484 backend is responsible for the final Home Assistant unlock call.
|
|
|
|
Remaining when hardware arrives:
|
|
|
|
1. Copy `firmware/esp32-pn532-door/include/l484_door_config.example.h` to `l484_door_config.h` and fill in local values.
|
|
2. Flash ESP32 and confirm PN532 card reads over serial.
|
|
3. Pair Zooz ZST39 LR and Kwikset 918 in Home Assistant/Z-Wave JS.
|
|
4. Create the Home Assistant webhook automation and set `HOME_ASSISTANT_UNLOCK_WEBHOOK_URL`.
|
|
5. Test with one development member, one active card, and one suspended member.
|