rforssen.net / projects / google-home

Google Home Integration

Controlling any device via Google Assistant using a self-hosted Flask OAuth2 server
01

Overview

working google home oauth2 self-hosted

This integration allows any device controlled by api.rforssen.net to be controlled via Google Home and Google Assistant voice commands. No Home Assistant, no Nabu Casa, no third-party services — just a Flask OAuth2 server and a Google Smart Home Action.

Architecture

"Hey Google, turn on the lights" Google Assistant api.rforssen.net/wled/smarthome Device API Physical device

Key facts

Google project
wled-proj
Fulfillment URL
/wled/smarthome
Authorize URL
/wled/oauth/authorize
Token URL
/wled/oauth/token
DB
wled_oauth (MySQL)
02

OAuth2 concepts

For normal Google login (rforssen.net), your app is the OAuth2 client — Google issues tokens to you. For Google Home, the roles are reversed: your app is the OAuth2 server — you issue tokens to Google.

Account linking flow

Google Home app Your /authorize page User clicks Allow Auth code → Google Google calls /token Access + refresh tokens stored

Token types

Access token — short-lived (1 hour). Google sends this with every EXECUTE/QUERY request as a Bearer token. Your API validates it against the access_tokens table.

Refresh token — long-lived (90 days). Google uses this to get new access tokens without re-linking. Your API handles this in the /token endpoint with grant_type=refresh_token.

Auth code — one-time use (10 minutes). Generated when user clicks Allow, exchanged immediately for tokens, then deleted.

Client credentials

The Client ID and Client Secret in the Google Developer Console are credentials YOU invent and give to Google. Google sends them back when calling your /token endpoint so your API can verify the request is genuinely from Google.

03

Files and endpoints

  • PY api/wled/__init__.py — Blueprint registration + auto-provisioning on startup
  • PY api/wled/db.py — MySQL connection + table provisioning (auth_codes, access_tokens, refresh_tokens)
  • PY api/wled/oauth_server.py — OAuth2 server: /wled/oauth/authorize + /wled/oauth/token
  • PY api/wled/smarthome.py — Smart Home fulfillment: SYNC / QUERY / EXECUTE intents
  • PY api/wled/wled_control.py — Device-specific control endpoints (WLED JSON API wrapper)

MySQL tables (wled_oauth)

auth_codes
10 min TTL, one-time use
access_tokens
1 hour TTL
refresh_tokens
90 day TTL

Tables are auto-created on pod startup via db.provision() called from __init__.py. The MySQL database itself must be created manually by root — the app user only has table-level privileges.

04

OAuth2 server implementation

GET /wled/oauth/authorize

Called by Google Home app when user initiates account linking. If the user is not logged in, redirects to /auth/login/google preserving all query parameters. Once logged in, shows the "Allow Google Home" consent page.

POST /wled/oauth/authorize

User clicked Allow. Validates client ID, generates a secrets.token_urlsafe(32) auth code, stores it in auth_codes table, redirects to Google's redirect URI with the code and state.

# Google's redirect URI format
https://oauth-redirect.googleusercontent.com/r/<project-id>

POST /wled/oauth/token

Called by Google to exchange an auth code for tokens, or to refresh an expired access token.

# authorization_code grant
grant_type=authorization_code
code=<auth_code>
redirect_uri=<redirect_uri>
client_id=<your_client_id>
client_secret=<your_client_secret>

# refresh_token grant
grant_type=refresh_token
refresh_token=<refresh_token>
client_id=<your_client_id>
client_secret=<your_client_secret>

Returns:

{
  "access_token":  "...",
  "token_type":    "Bearer",
  "expires_in":    3600,
  "refresh_token": "...",
  "scope":         "..."
}
05

Smart Home fulfillment

All Google Assistant commands arrive at POST /wled/smarthome as JSON with a Bearer token. The endpoint validates the token against access_tokens table, then handles three intents:

SYNC — device discovery

Called when Google needs to know what devices exist. Returns a device descriptor. To add a new device, add an entry to the devices array.

{
  "id":   "wled-1",
  "type": "action.devices.types.LIGHT",
  "traits": [
    "action.devices.traits.OnOff",
    "action.devices.traits.Brightness"
  ],
  "name": {
    "name": "LED Strip",
    "nicknames": ["lights", "the lights", "leds"]
  }
}

QUERY — current state

Called when Google needs to know the current state of a device (e.g. "Hey Google, are the lights on?"). Returns on/off status and brightness.

EXECUTE — commands

Called when user issues a voice command. Supported commands:

OnOff
turn on / turn off
BrightnessAbsolute
set brightness to X%

Token validation

# Every request to /smarthome must include:
Authorization: Bearer <access_token>

# Validated against MySQL:
SELECT user_sub FROM access_tokens
WHERE token = %s AND expires_at > UTC_TIMESTAMP()
06

Google Developer Console setup

One-time setup at console.home.google.com. Create a project → Add cloud-to-cloud integration → Develop.

Account linking fields

OAuth Client ID
You invent this string
Client Secret
You invent this string
Authorization URL
https://api.rforssen.net/wled/oauth/authorize
Token URL
https://api.rforssen.net/wled/oauth/token
Fulfillment URL
https://api.rforssen.net/wled/smarthome

The Client ID and Secret you enter here must match GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET in oauth_server.py. Google sends them when calling your /token endpoint.

After saving

Go to Test tab → the integration appears as "Ready". Account linking must be done from the Google Home app before testing.

07

Account linking

Account linking connects your Google account to your Smart Home Action. Must be done once from the Google Home app.

Steps

Open Google Home app → tap + → Set up device → Works with Google → search for [test] wled-proj → tap it → the OAuth2 flow starts → you see the "Authorize Google Home" page → click Allow → linking complete.

After linking

Google issues a SYNC request to discover devices. Your devices appear in Google Home. Voice commands work immediately.

Re-syncing devices

If you add or rename devices in smarthome.py, say "Hey Google, sync my devices" to force a re-sync.

Troubleshooting

If account linking fails with "Not Found" — check that the user is logged in to rforssen.net before the authorize page is reached. The authorize endpoint redirects to /auth/login/google if no session exists, preserving all query parameters in the redirect URL.

If linking fails with unauthorized_client — the Client ID in oauth_server.py doesn't match what's in the Google Developer Console.

08

Adding new devices

To add a new device (e.g. a custom ESP32 animation controller, a second WLED strip, a relay):

1. Add the device to SYNC

In smarthome.py, add an entry to the devices array in the SYNC handler. Give it a unique id, appropriate type and traits, and friendly name/nicknames.

2. Handle it in EXECUTE

In the EXECUTE handler, route by device ID:

if device_id == "wled-1":
    # call WLED API
elif device_id == "esp32-custom":
    # call your custom ESP32 API

3. Handle it in QUERY

Return the current state for each device ID.

4. Sync Google

Say "Hey Google, sync my devices" — the new device appears in Google Home immediately.

Device types reference

LIGHT
action.devices.types.LIGHT
SWITCH
action.devices.types.SWITCH
OUTLET
action.devices.types.OUTLET
FAN
action.devices.types.FAN

Full list: developers.home.google.com/cloud-to-cloud/guides