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:
- Run a small one-shot helper that talks to Cognito directly (
pycognito,boto3.client('cognito-idp').initiate_auth, or the community projectpyschlage). Use the Cognito Client ID26ujbcfppen6t3afts2ad7vfgsin theus-west-2user pool. The response carriesIdTokenandRefreshTokenunderAuthenticationResult. - 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
- Mint tokens. Use one of the bootstrap recipes above to produce an
IdToken(and ideally aRefreshToken) for your Schlage Home account. - Add the device. In Devices, create a new device with driver
schlage_encode. Pasteaccess_token(secure) and, if available,refresh_token(secure). Leave the other attributes on their defaults. - 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 adeviceIdUUID and a friendly name. - Create zones. For each lock you want to control from GEM, create a zone under a lock / security subsystem. Set
zone.addressto the lock'sdeviceIdUUID. The zone state will populate aslockedorunlockedonce the next poll runs. - Verify. Use the Script Console to send
lockandunlockand confirm the lock responds within a few seconds.
Attributes
Device — required
| Name | Type | Description |
|---|---|---|
access_token | string (secure) | Cognito IdToken minted for your Schlage Home account. |
Device — optional
| Name | Type | Default | Description |
|---|---|---|---|
refresh_token | string (secure) | — | Cognito RefreshToken. When set, the driver auto-refreshes the access token before each session and on 401 responses. Strongly recommended. |
cognito_client_id | string | 26ujbcfppen6t3afts2ad7vfgs | AWS Cognito app client id. Community-derived constant. |
cognito_region | string | us-west-2 | AWS region for the Cognito user pool. |
api_base | string | https://api.allegion.yonomi.cloud | Allegion partner cloud endpoint. |
api_key | string (secure) | well-known constant | x-api-key header value. Defaults to the value shipped in community projects and the Schlage Home iOS app. |
status_interval | int (ms) | 60000 | How 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_timeout | int (ms) | 15000 | HTTP request timeout. A blank, zero, negative or unreadable value uses 15 s. |
Zone — address
| Field | Description |
|---|---|
address | The 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
| Name | Args | Description |
|---|---|---|
lock | address | Remotely lock the specified lock. |
unlock | address | Remotely unlock the specified lock. |
get_status | address | Return the current lock state with the full cloud detail payload. |
get_devices | — | List all locks visible to the account. |
get_device_detail | address | Return 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_tokenis picked up at the next retry. - Expired tokens. A 401 triggers one Cognito refresh (when
refresh_tokenis set) and the request is sent once more with the new token;lock/unlockset 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 tostatus_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:
| Attribute | Values |
|---|---|
state | locked, unlocked, or unchanged if the cloud reports unknown. |
battery_level | 0-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_tokento enable auto-refresh, or paste a freshly-minted access token. Tokens last ~60 min. get_devicesreturns an empty list. The account has no locks paired, the lock is offline, or the token belongs to a different Schlage Home account.lock/unlockreturns 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_intervalis 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 issuedlock/unlockto confirm the command landed.) - Cognito refresh fails. Either the
refresh_tokenis stale (Cognito invalidates RTs after long inactivity or password change) orcognito_client_idhas rotated. Therefresh_tokencommand returns the Cognito error type (for examplecognito 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
lockStatesemantics vary slightly between Encode firmware revisions; the driver normalises 0/1 (and the stringsunlocked/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.