Skip to main content

Schlage Encode / Sense+

GEM driver for Schlage Wi-Fi smart locks via the Allegion (Yonomi-derived) partner cloud REST API. Covers the Encode Wi-Fi family (Encode, Encode Plus, Encode Smart) and Sense+ when paired to a Wi-Fi bridge. Supports remote lock/unlock and periodic state polling; battery level is mirrored onto the zone when reported.

Prerequisites​

  • A Schlage Home account with the lock already enrolled in the Schlage Home iOS / Android app. The lock must be online and respond to the app before integrating with GEM.
  • An access token (Cognito IdToken) minted from your Schlage Home credentials.
  • Optionally, a refresh token from the same Cognito login. Without it, the access token expires after ~60 minutes and has to be re-pasted manually.

The Allegion cloud sits behind AWS Cognito with USER_SRP_AUTH; there is no public OAuth flow. The SRP login is not implemented inside the driver to keep its scope small. Two options to mint a token:

  1. Run a small one-shot helper that talks to Cognito directly (pycognito, boto3.client('cognito-idp').initiate_auth, or the community project pyschlage). Use the Cognito Client ID 26ujbcfppen6t3afts2ad7vfgs in the us-west-2 user pool. The response carries IdToken and RefreshToken under AuthenticationResult.
  2. Capture the Authorization: Bearer … header from a Schlage Home app request using Charles / mitmproxy. Less reliable but works in a pinch.

Paste the resulting IdToken as the access_token attribute. If you have the RefreshToken, paste it as refresh_token and the driver will renew the access token silently.

Setup steps​

  1. Mint tokens. Use one of the bootstrap recipes above to produce an IdToken (and ideally a RefreshToken) for your Schlage Home account.
  2. Add the device. In Devices, create a new device with driver schlage_encode. Paste access_token (secure) and, if available, refresh_token (secure). Leave the other attributes on their defaults.
  3. Discover locks. Open the device's Script Console tab and run await gem.command({device_id: <device_id>, command: 'get_devices'}). The response is an array of locks, each with a deviceId UUID and a friendly name.
  4. Create zones. For each lock you want to control from GEM, create a zone under a lock / security subsystem. Set zone.address to the lock's deviceId UUID. The zone state will populate as locked or unlocked once the next poll runs.
  5. Verify. Use the Script Console to send lock and unlock and confirm the lock responds within a few seconds.

Attributes​

Device — required​

NameTypeDescription
access_tokenstring (secure)Cognito IdToken minted for your Schlage Home account.

Device — optional​

NameTypeDefaultDescription
refresh_tokenstring (secure)—Cognito RefreshToken. When set, the driver auto-refreshes the access token before each session and on 401 responses. Strongly recommended.
cognito_client_idstring26ujbcfppen6t3afts2ad7vfgsAWS Cognito app client id. Community-derived constant.
cognito_regionstringus-west-2AWS region for the Cognito user pool.
api_basestringhttps://api.allegion.yonomi.cloudAllegion partner cloud endpoint.
api_keystring (secure)well-known constantx-api-key header value. Defaults to the value shipped in community projects and the Schlage Home iOS app.
status_intervalint (ms)60000How often to refresh lock state from the cloud. Values below 15 s (including negative ones) are raised to 15 s; a blank, 0, unreadable or impossibly large value uses 60 s.
request_timeoutint (ms)15000HTTP request timeout. A blank, zero, negative or unreadable value uses 15 s.

Zone — address​

FieldDescription
addressThe Schlage cloud deviceId UUID for the lock (e.g. 11111111-2222-3333-4444-555555555555). Discover via get_devices. Leading and trailing spaces are ignored.

Commands​

NameArgsDescription
lockaddressRemotely lock the specified lock.
unlockaddressRemotely unlock the specified lock.
get_statusaddressReturn the current lock state with the full cloud detail payload.
get_devices—List all locks visible to the account.
get_device_detailaddressReturn the full cloud detail payload for a lock (battery, firmware, last action).
refresh_token—Force a Cognito token refresh now (requires refresh_token to be set).

Commands that take address refuse a missing or blank address (address required) and an unfilled template token such as [@zone.address] (address is an unfilled token: …) without contacting the cloud. A lock or unlock only reports success when the cloud answers with a 2xx; anything else comes back as {error, status, data} — including a 2xx HTML page from a captive portal or proxy (unexpected html reply).

Connection, tokens and polling​

  • Connecting. The driver lists the account's locks to confirm the token works. If the cloud cannot be reached or rejects the token, it retries on its own after 30 s, doubling to every 5 minutes, so a controller that boots during an internet outage recovers without a reload, and a freshly pasted access_token is picked up at the next retry.
  • Expired tokens. A 401 triggers one Cognito refresh (when refresh_token is set) and the request is sent once more with the new token; lock / unlock set an absolute state, so the resend is safe. Requests that hit a 401 at the same moment share a single refresh. If the token is still refused, the device is marked disconnected and stays disconnected until a request succeeds again.
  • Polling. One lock is read every 750 ms per pass. When every lock fails for three passes in a row because the cloud is unreachable, rejecting the token, rate limiting or erroring, the wait between passes doubles up to 5 minutes (or status_interval, if that is longer); the first successful reply returns it to status_interval. A lock that is merely offline or unknown to the account does not slow the others down. Repeated identical errors are logged once, with a periodic summary.

Zone state attributes​

The driver writes these attributes onto the GEM zone:

AttributeValues
statelocked, unlocked, or unchanged if the cloud reports unknown.
battery_level0-100 percent, when the lock reports a numeric batteryLevel. Firmware variant.

What we don't yet support​

  • Cognito SRP login from inside GEM. Tokens are minted out-of-band; see the bootstrap recipe above. Implementing SRP inline would add ~150 LOC of crypto and is best done as a separate utility.
  • Access codes / users. The Allegion API exposes per-lock guest codes and user management, but those endpoints are not yet wired through this driver — only lock state.
  • Real-time push. Allegion does not publish a public WebSocket / push channel. State changes from the lock face show up after the next polling tick (default 60 s).
  • Sense locks without Wi-Fi. Pure Bluetooth Schlage Sense locks have no cloud presence; they cannot be controlled remotely.

Troubleshooting​

  • 401 on every request. The access token has expired. Set refresh_token to enable auto-refresh, or paste a freshly-minted access token. Tokens last ~60 min.
  • get_devices returns an empty list. The account has no locks paired, the lock is offline, or the token belongs to a different Schlage Home account.
  • lock / unlock returns 409 or 503. The lock has lost Wi-Fi connectivity. Check the Schlage Home app — if the lock shows "offline" there it cannot be controlled remotely either.
  • State stays stale after a manual unlock at the lock. Confirm status_interval is not set above a few minutes. The cloud only reflects local lock actions after the next successful poll. (The driver also re-polls 2.5 s after every issued lock/unlock to confirm the command landed.)
  • Cognito refresh fails. Either the refresh_token is stale (Cognito invalidates RTs after long inactivity or password change) or cognito_client_id has rotated. The refresh_token command returns the Cognito error type (for example cognito refresh http 400: NotAuthorizedException). Re-bootstrap to mint a fresh token pair.

Honest caveats​

  • The api_key default mirrors a value that has circulated in community projects since the Yonomi era. Allegion has not formally published a partner program. If the cloud starts returning 403, the constant may have rotated.
  • Implementations of lockState semantics vary slightly between Encode firmware revisions; the driver normalises 0/1 (and the strings unlocked/locked) but anything else is treated as unknown rather than coerced.
  • Without real hardware on the bench, the lock/unlock command body ({attributes: {lockState: 0|1}}) is the shape the Allegion v2 endpoint accepts in the documented integrations — confirm against your install before relying on macros.