Skip to content

Instantly share code, notes, and snippets.

@brennanMKE
Last active January 13, 2025 07:22
Show Gist options
  • Save brennanMKE/6a6960a24b4c406427e4def2ac5037ab to your computer and use it in GitHub Desktop.
Save brennanMKE/6a6960a24b4c406427e4def2ac5037ab to your computer and use it in GitHub Desktop.
Schlage Smart WiFi Deadbolt: Access Control

Schlage Smart WiFi Deadbolt

Access Control

This document provides the detailed steps and Python code required to set up and manage the Schlage smart lock system for multi-lock access control, dynamic PIN management, and member revocation.


1. Overview

This system integrates a Schlage smart lock system with a REST API for member management. It includes:

  1. Dynamic 8-Digit PIN Management:
    • Format:
      • 1st digit: Lock ID.
      • 2nd–4th digits: User ID.
      • 5th–8th digits: Unique PIN for each user.
  2. Multi-Lock Support:
    • Each lock is identified by a unique single-digit lock_id.
  3. Member Revocation:
    • Members marked as inactive in the REST API will have their PINs removed from the specified lock.
  4. Member PIN Updates:
    • Allows members to change their PINs securely.

2. Requirements

2.1 Software

  1. Python Libraries:

    • pyschlage: For Schlage lock management.
    • requests: For interacting with the REST API. Install dependencies:
    pip install pyschlage requests
  2. REST API:

    • Endpoint: GET /api/members.
    • JSON Response:
      [
        {"name": "Amber Simons", "user_id": "139", "active": true},
        {"name": "Mark Walters", "user_id": "455", "active": false},
        {"name": "Maggie Campbell", "user_id": "122", "active": true}
      ]

2.2 Hardware

  • Schlage WiFi locks, integrated with the pyschlage Python library.

3. Code Implementation

3.1 Member PIN Management

import random
import requests
from pyschlage import Auth, Schlage
from pyschlage.code import AccessCode

# API endpoint for member data
API_URL = "https://example.com/api/members"

# Authentication for Schlage lock
AUTH = Auth(username="your_username", password="your_password")
schlage = Schlage(AUTH)

# Retrieve a lock by lock_id
def get_lock_by_id(lock_id):
    for lock in schlage.locks():
        if lock.name.endswith(str(lock_id)):  # Assuming lock names include the lock ID
            return lock
    raise Exception(f"Lock with ID {lock_id} not found.")

# Fetch current members from REST API
def fetch_members():
    response = requests.get(API_URL)
    response.raise_for_status()
    return response.json()

# Generate a unique 4-digit PIN
def generate_unique_pin(existing_pins):
    while True:
        pin = f"{random.randint(1000, 9999)}"
        if pin not in existing_pins:
            return pin

# Update lock with member PINs
def update_pins(lock_id):
    lock = get_lock_by_id(lock_id)
    members = fetch_members()

    existing_pins = []
    updated_pins = []

    for member in members:
        full_pin = None
        if member["active"]:
            # Generate or reuse a PIN for active members
            four_digit_pin = generate_unique_pin(existing_pins)
            existing_pins.append(four_digit_pin)

            # Create the 8-digit PIN
            full_pin = f"{lock_id}{member['user_id']}{four_digit_pin}"

            # Update or add the access code
            access_code = AccessCode(name=member["name"], code=full_pin)
            lock.add_access_code(access_code)

            updated_pins.append({"name": member["name"], "pin": full_pin})
        else:
            # Revoke access for inactive members
            for code_id, access_code in lock.access_codes.items():
                if access_code.name == member["name"]:
                    access_code.delete()

    # Save updated pins to a file
    with open(f"lock_{lock_id}_pins.txt", "w") as file:
        for pin_info in updated_pins:
            file.write(f"{pin_info['name']} - PIN: {pin_info['pin']}\n")

    print(f"Pins updated for lock ID {lock_id}. Saved to 'lock_{lock_id}_pins.txt'.")

# Change a member's PIN
def change_member_pin(lock_id, user_id, current_full_pin, new_four_digit_pin):
    lock = get_lock_by_id(lock_id)

    # Create the new 8-digit PIN
    new_full_pin = f"{lock_id}{user_id}{new_four_digit_pin}"

    # Fetch existing access codes
    for code_id, access_code in lock.access_codes.items():
        if access_code.code == current_full_pin:
            # Update the access code with the new PIN
            access_code.code = new_full_pin
            access_code.save()
            print(f"PIN for user ID {user_id} updated successfully in lock ID {lock_id}.")
            return

    raise Exception("Current PIN not found or invalid.")

4. Usage Instructions

4.1 Initial Setup

To update all member PINs for a specific lock:

update_pins(lock_id=1)
  • This will:
    • Fetch the member list from the REST API.
    • Generate 8-digit PINs for active members.
    • Remove access for inactive members.
    • Save the updated PINs to lock_1_pins.txt.

4.2 Change a Member’s PIN

To change a member’s PIN for a specific lock:

change_member_pin(lock_id=1, user_id="139", current_full_pin="11394827", new_four_digit_pin="5678")
  • Inputs:
    • lock_id: Lock ID for the target lock.
    • user_id: 3-digit user ID.
    • current_full_pin: Current 8-digit PIN.
    • new_four_digit_pin: New 4-digit PIN for the user.

4.3 Example PIN File

After running update_pins, the file lock_1_pins.txt might look like:

Amber Simons - PIN: 11394827
Maggie Campbell - PIN: 11229145

5. Security Considerations

  1. Secure REST API Access:

    • Use HTTPS for the REST API.
    • Authenticate API requests with tokens.
  2. PIN Management:

    • Ensure PIN uniqueness.
    • Encrypt lock_{lock_id}_pins.txt or restrict its access.
  3. Audit Logs:

    • Enable and monitor lock logs for PIN usage.
  4. Periodic PIN Rotation:

    • Regularly update member PINs for enhanced security.

This system ensures efficient and secure access control with support for multi-lock management, dynamic PIN updates, and real-time member revocation. For questions or setup assistance, feel free to reach out!

To ensure security, you should disallow the use of common and easily guessable 4-digit PINs. Here’s a list of such PINs to block:

  1. Sequential Numbers:

    • 1234
    • 2345
    • 3456
    • 4567
    • 5678
    • 6789
    • 7890
    • 0123
  2. Repeated Digits:

    • 1111
    • 2222
    • 3333
    • 4444
    • 5555
    • 6666
    • 7777
    • 8888
    • 9999
    • 0000
  3. Mirrored Patterns:

    • 1221
    • 2112
    • 3443
    • 4334
    • 5665
    • 6556
    • 7887
    • 8778
    • 9009
  4. Common Years:

    • 1990
    • 2000
    • 2020
    • 1980
    • 1970
  5. Popular PINs Identified in Studies:

    • 2580 (numbers straight down the keypad)
    • 1212
    • 6969
    • 9876 (reverse sequential)
  6. Common Date Patterns (MMDD):

    • 0101 (January 1st)
    • 1212 (December 12th)
    • 0704 (July 4th)
    • 1225 (December 25th)
    • 1031 (October 31st)

By blocking these patterns and combinations, you significantly enhance the security of your system. Encourage users to create unique, less predictable codes.

NFC Tag

Hold an iPhone or Android phone near an NFC tag can trigger the deadbolts to be unlocked using an app which is already authenticated which would make a request which can run the Python script to unlock the Schlage deadbolt.

iPhone and Android Compatibility

  1. NFC Functionality:

    • Both iOS (iPhone 7 and newer) and Android devices support NFC functionality.
    • NFC tags can trigger an action within a custom mobile app when scanned. For iPhones, this functionality requires the app to be in the foreground unless the NFC tag is specially configured to use an NDEF payload to open the app.
  2. Authentication:

    • The app can authenticate the user using their login credentials or stored tokens.
    • The pyschlage library can then interact with your Schlage locks using its APIs for authentication and lock control.
  3. Unlocking Logic:

    • The app can use the NFC-triggered action to validate the user's membership against your Makerspace's active user database.
    • After validation, the app would issue an unlock command to the respective Schlage lock using the library’s unlock() method.

Apple Watch Integration

  1. NFC Reader:

    • Apple Watch Series 4 and later include NFC capabilities.
    • Users could trigger the same app action by tapping their watch near the NFC tag.
  2. App Synchronization:

    • The Apple Watch app would sync with the iPhone counterpart, sharing credentials or session tokens.
    • The app would perform the same unlock sequence as described above.

Additional Benefits

  • Ease of Use: Users won’t need to remember or input an 8-digit code. Instead, they can unlock doors with a quick tap.
  • Enhanced Security:
    • Using location-based services (geofencing) can add an additional layer of validation.
    • Apps can use modern authentication, like biometrics (Face ID, Touch ID), before allowing the unlock request.

Development Requirements

  • A mobile app (for both iOS and Android) to handle NFC reading, authentication, and communication with the Schlage API.
  • NFC tags configured to trigger app actions (optional: preloaded with app launch URLs or specific identifiers).
  • Backend integration with your Makerspace's membership database for real-time validation.

Adding an NDEF (NFC Data Exchange Format) payload to an NFC tag can enable it to launch a specific mobile app and even pass data to the app to trigger specific actions. Here's a detailed explanation:


What is an NDEF Payload?

An NDEF payload is a standardized data format used to encode information onto NFC tags. For your use case, it would contain information that:

  1. Launches the app: Directs the phone to open a specific app.
  2. Triggers an action: Passes parameters (like a command or identifier) to the app upon launch.

Components of NDEF Payload

  1. App Launch:

    • Android: Uses the "Android Application Record (AAR)" to launch an app.
    • iOS: Encodes a Universal Link (URL) associated with the app. When the NFC tag is scanned, iOS automatically resolves the URL and opens the app (if installed).
  2. Data to Trigger Actions:

    • Data can be encoded within the NDEF payload, such as:
      • A unique identifier for the NFC tag.
      • A specific command (e.g., "unlock door").
      • Metadata to validate the request (e.g., tag location ID).

Steps to Configure an NDEF Payload

  1. Choose an NFC Tag:

    • Use writable NFC tags with sufficient memory to store your payload (e.g., NTAG213 for small payloads or NTAG215 for larger payloads).
  2. Write the NDEF Payload:

    • Use an NFC writer app (e.g., "NFC Tools" on Android or iOS).

    • Format the tag to include:

      • For iOS: A Universal Link that directs to your app.
      • For Android: An AAR with your app's package name or an Intent URL.

      Example NDEF content:

      • URI Record: https://yourapp.com/unlock?doorId=1234
      • AAR (Android): Ensures Android attempts to open your app directly.
  3. Integrate Universal Links or Deep Links in Your App:

    • Universal Links (iOS):
      • Set up a domain (e.g., yourapp.com) with an apple-app-site-association file on the server.
      • The file maps the Universal Link to your app.
    • Deep Links (Android):
      • Update your app's AndroidManifest.xml file to handle the Intent or URL scheme passed by the tag.
  4. Read and Parse the Data in the App:

    • Implement code in your app to handle the incoming link or intent.
    • Example for iOS:
      func application(_ app: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
          if let url = userActivity.webpageURL {
              handleURL(url) // Parse the URL and trigger the unlock action
          }
          return true
      }
    • Example for Android:
      @Override
      protected void onNewIntent(Intent intent) {
          super.onNewIntent(intent);
          String action = intent.getAction();
          if (NfcAdapter.ACTION_NDEF_DISCOVERED.equals(action)) {
              // Handle the NFC data
              Uri uri = intent.getData();
              handleUnlockAction(uri);
          }
      }

Testing and Validation

  1. Test the Tag:

    • Use the NFC writer app to write and test the tag with your app.
    • Confirm that scanning the tag launches the app and triggers the expected action.
  2. Security Measures:

    • Validate the incoming data in your app to ensure it's authentic.
    • Require user authentication (e.g., biometrics) before executing the unlock command.

Benefits of NDEF Payload

  • Convenience: Users simply scan the tag to open the app and perform the unlock action.
  • Platform Compatibility: Works on both iOS and Android.
  • Extensibility: Allows for additional functionality (e.g., logging, geofencing) by embedding metadata in the payload.

First Time Setup

Here’s a Python script and instructions to set up a system for generating and setting unique PINs for each member based on a local tab-delimited file. This script uses the pyschlage library to manage access codes for a Schlage lock, with privacy considerations in mind by using the User ID instead of names for AccessCode.


Instructions for Setting Up the Script

1. Prerequisites

  1. Ensure you have Python 3.9 or later installed on your macOS system.
  2. Install the required Python libraries:
    pip install pyschlage

2. Prepare the Local Member File

  1. Create a tab-delimited file (e.g., members.txt) with the following format:
    Name	UserID	Active
    Amber Simons	139	True
    Mark Walters	455	False
    Maggie Campbell	122	True
    
  2. Save the file in the same directory as the script.

3. Python Script

Save the following script as generate_and_set_pins.py:

import random
from pyschlage import Auth, Schlage
from pyschlage.code import AccessCode

# Path to the member file
MEMBER_FILE = "members.txt"
OUTPUT_FILE = "lock_1_pins.txt"

# Schlage account credentials
USERNAME = "your_username"
PASSWORD = "your_password"

# Lock ID
LOCK_ID = 1

# Authenticate with Schlage
def authenticate():
    return Schlage(Auth(username=USERNAME, password=PASSWORD))

# Read members from the local file
def read_members(file_path):
    members = []
    with open(file_path, "r") as file:
        next(file)  # Skip the header row
        for line in file:
            name, user_id, active = line.strip().split("\t")
            members.append({
                "name": name,
                "user_id": user_id,
                "active": active.lower() == "true"
            })
    return members

# Generate a unique 4-digit PIN
def generate_unique_pin(existing_pins):
    while True:
        pin = f"{random.randint(1000, 9999)}"
        if pin not in existing_pins:
            return pin

# Update lock with generated PINs
def update_pins(lock, members):
    existing_pins = []
    updated_pins = []

    for member in members:
        if member["active"]:
            # Generate unique 4-digit PIN
            four_digit_pin = generate_unique_pin(existing_pins)
            existing_pins.append(four_digit_pin)

            # Create 8-digit PIN (Lock ID + User ID + 4-digit PIN)
            full_pin = f"{LOCK_ID}{member['user_id']}{four_digit_pin}"

            # Add access code to the lock
            access_code = AccessCode(name=member["user_id"], code=full_pin)
            lock.add_access_code(access_code)

            updated_pins.append({"user_id": member["user_id"], "pin": full_pin})
        else:
            # Revoke access for inactive members
            for code_id, access_code in lock.access_codes.items():
                if access_code.name == member["user_id"]:
                    access_code.delete()

    # Save updated PINs to a file
    with open(OUTPUT_FILE, "w") as file:
        for pin_info in updated_pins:
            file.write(f"User ID: {pin_info['user_id']} - PIN: {pin_info['pin']}\n")

    print(f"Pins updated for lock ID {LOCK_ID}. Saved to '{OUTPUT_FILE}'.")

# Main function
def main():
    # Authenticate and fetch the lock
    schlage = authenticate()
    lock = None
    for l in schlage.locks():
        if l.name.endswith(str(LOCK_ID)):  # Assuming lock names include the lock ID
            lock = l
            break

    if not lock:
        raise Exception(f"Lock with ID {LOCK_ID} not found.")

    # Read members and update PINs
    members = read_members(MEMBER_FILE)
    update_pins(lock, members)

if __name__ == "__main__":
    main()

How to Use the Script

  1. Update Your Credentials:

    • Replace "your_username" and "your_password" with your Schlage account credentials in the USERNAME and PASSWORD variables.
  2. Prepare the Member File:

    • Ensure the members.txt file is formatted correctly and placed in the same directory as the script.
  3. Run the Script:

    python generate_and_set_pins.py
  4. Output File:

    • The script generates a file lock_1_pins.txt containing the updated PINs for active members:
      User ID: 139 - PIN: 11394827
      User ID: 122 - PIN: 11229145
      

Features

  • Unique PIN Generation:

    • Ensures no duplicate PINs for members of the same lock.
  • Member Revocation:

    • Removes access for inactive members.
  • Privacy Preservation:

    • Uses User ID for AccessCode to avoid exposing member names.
  • Output Logging:

    • Saves generated PINs to a file for auditing.

Let me know if you need assistance customizing this further!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment