To connect a Raspberry Pi to Google Home without relying on deprecated third-party applets or bulky hub software, the most reliable method in 2026 is building a local FastAPI webhook server on a Raspberry Pi Zero 2 W and exposing it via a Cloudflare Tunnel to a Google Smart Home Action. This architecture bypasses local network NAT issues, gives you sub-200ms voice-to-relay latency, and provides direct, scriptable GPIO control using Python.
Estimated Time: 2.5 hours (Hardware: 30m, Code: 60m, Google Cloud Setup: 60m)
The Integration Decision Path
Before wiring a single pin, you must choose the communication protocol between Google's cloud and your Pi. Here is the decision matrix for custom hardware integration:
| Integration Method | Complexity | Latency | Requirement | Verdict |
|---|---|---|---|---|
| Matter over Thread | High | ~100ms | Thread Border Router, C++ SDK | Overkill for single-node Pi |
| Google Assistant SDK (Local) | Medium | ~300ms | OAuth2, Audio streaming | Deprecated for custom traits |
| Cloud Webhook (FastAPI) | Low-Med | ~150ms | Cloudflare Tunnel, Python | DEFAULT PICK |
Decision Termination: Unless you are manufacturing a commercial Thread-enabled product, choose the Cloud Webhook method. It requires zero port-forwarding, costs $0, and uses standard REST/JSON payloads that are trivial to debug.
Hardware Spec Sheet and Pin Mapping
This build targets the Raspberry Pi Zero 2 W running the 64-bit Raspberry Pi OS (Bookworm or newer). The Zero 2 W's quad-core processor easily handles FastAPI JSON parsing without the thermal throttling issues seen on the original single-core Zero.
| Component | Exact Variant / Model | Estimated Cost (2026) |
|---|---|---|
| Microcontroller | Raspberry Pi Zero 2 W (with headers) | $15.00 |
| Storage | SanDisk Extreme 32GB microSD (A1 rated) | $9.00 |
| Relay Module | 2-Channel 5V Opto-isolated (Songle SRD-05VDC-SL-C) | $6.50 |
| Wiring | 22 AWG Silicone Stranded Wire | $4.00 |
| Power Supply | Official Raspberry Pi 5V 2.5A USB-C Adapter | $12.00 |
Pin Mapping Table
We use hardware PWM-capable pins for flexibility, though standard digital output is sufficient for relays. Note that most opto-isolated relay modules are Active LOW, meaning the GPIO pin must pull to ground to energize the coil.
| Pi Zero 2 W Pin | BCM GPIO | Relay Module Pin |
|---|---|---|
| Pin 11 | GPIO 17 | IN1 (Relay 1) |
| Pin 13 | GPIO 27 | IN2 (Relay 2) |
| Pin 2 (5V) | 5V Power | VCC |
| Pin 6 (GND) | Ground | GND |
Wiring the Opto-Isolated Relay Node
- Prepare the Pi: Solder the 40-pin header to the Pi Zero 2 W if not pre-installed. Flash the 64-bit Raspberry Pi OS Lite (headless) using Raspberry Pi Imager, enabling SSH and your local WiFi credentials in the OS customization menu.
- Wire VCC and GND: Connect Pi Pin 2 (5V) to the Relay Module VCC. Connect Pi Pin 6 (GND) to the Relay Module GND. Bench tip: Do not power the relay coil directly from the Pi's 3.3V pin; the 5V rail is required for the Songle 5V coil, and the opto-isolator protects the Pi from back-EMF.
- Wire the Signal Pins: Connect Pi Pin 11 (GPIO 17) to IN1, and Pi Pin 13 (GPIO 27) to IN2.
- Verify the Active-LOW Logic: Boot the Pi. Using a multimeter, measure the voltage between the IN1 pin and GND. It should read ~3.3V (High/Off). When triggered via software, it will drop to ~0.1V (Low/On).
The FastAPI Webhook Code (Pi Zero 2 W Target)
This code targets the Raspberry Pi Zero 2 W running 64-bit OS. It uses FastAPI for the web server and gpiozero for hardware control. The gpiozero library is preferred over RPi.GPIO in modern 64-bit Pi OS environments because it natively supports the lgpio backend required by the newer Linux kernel GPIO character device interface.
Prerequisites: Run sudo apt install python3-lgpio python3-pip and pip3 install fastapi uvicorn gpiozero.
import logging
from fastapi import FastAPI, Request, HTTPException, Header
from gpiozero import OutputDevice
import uvicorn
# --- Pin Definitions ---
RELAY_1_PIN = 17
RELAY_2_PIN = 27
# --- Configuration ---
EXPECTED_TOKEN = "your_google_action_secret_token_here"
# Setup Logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(message)s')
app = FastAPI(title="Google Home Pi Relay Bridge")
# Initialize Relays (Active LOW for opto-isolated modules)
# initial_value=False ensures relays are OFF on boot
relay1 = OutputDevice(RELAY_1_PIN, active_high=False, initial_value=False)
relay2 = OutputDevice(RELAY_2_PIN, active_high=False, initial_value=False)
def verify_token(authorization: str = Header(None)):
if not authorization or authorization.replace("Bearer ", "") != EXPECTED_TOKEN:
raise HTTPException(status_code=401, detail="Unauthorized")
@app.post("/smarthome")
async def smart_home_intent(request: Request, authorization: str = Header(None)):
verify_token(authorization)
payload = await request.json()
intent = payload["inputs"][0]["intent"]
request_id = payload["requestId"]
logging.info(f"Received Intent: {intent}")
if intent == "action.devices.SYNC":
return {
"requestId": request_id,
"payload": {
"agentUserId": "pi-zero-user-01",
"devices": [
{
"id": "relay_1",
"type": "action.devices.types.SWITCH",
"traits": ["action.devices.traits.OnOff"],
"name": {"name": "Desk Lamp"},
"willReportState": False
},
{
"id": "relay_2",
"type": "action.devices.types.SWITCH",
"traits": ["action.devices.traits.OnOff"],
"name": {"name": "Soldering Fan"},
"willReportState": False
}
]
}
}
elif intent == "action.devices.QUERY":
return {
"requestId": request_id,
"payload": {
"devices": {
"relay_1": {"on": relay1.value, "online": True},
"relay_2": {"on": relay2.value, "online": True}
}
}
}
elif intent == "action.devices.EXECUTE":
commands = payload["inputs"][0]["payload"]["commands"]
response_commands = []
for command in commands:
device_ids = [d["id"] for d in command["devices"]]
execution = command["execution"][0]
cmd = execution["command"]
if cmd == "action.devices.commands.OnOff":
state = execution["params"]["on"]
for dev_id in device_ids:
if dev_id == "relay_1":
relay1.value = state
elif dev_id == "relay_2":
relay2.value = state
response_commands.append({
"ids": device_ids,
"status": "SUCCESS",
"states": {"on": state, "online": True}
})
return {"requestId": request_id, "payload": {"commands": response_commands}}
raise HTTPException(status_code=400, detail="Unknown Intent")
if __name__ == "__main__":
# Bind to localhost; Cloudflare Tunnel will handle external routing
uvicorn.run(app, host="127.0.0.1", port=8000)
127.0.0.1:8000 server to Google's cloud securely, install cloudflared on the Pi and create a tunnel pointing to your custom domain (e.g., pi-relay.yourdomain.com). Use this HTTPS URL as the Fulfillment endpoint in the Google Home Developer Console.
Debugging: Exact Error Strings and First 3 Checks
When bridging cloud intents to local hardware, failures usually happen at the JSON schema level or the Linux GPIO permission level. If Google Home responds with "Sorry, I couldn't reach your device," check these exact failure modes.
The First 3 Things to Check When It Fails
- Cloudflare Tunnel Routing: Run
curl -X POST https://your-domain.com/smarthomefrom an external machine. If you get a 502 Bad Gateway, thecloudflaredservice has crashed or the FastAPIuvicornprocess died. Checksystemctl status cloudflared. - GPIO Permissions (The lgpio Backend): If your Python script crashes on boot with a pin factory error, ensure your user is in the
dialoutandgpiogroups, and thatpython3-lgpiois installed via apt. The legacysysfsGPIO interface is disabled in modern 64-bit Pi OS. - Intent Payload Schema Mismatch: Google strictly validates the SYNC response. If you misspell
action.devices.types.SWITCHor omit thetraitsarray, the Google Home app will silently drop the device during the "Sync my devices" phase.
Ranked Causes for Specific Error Strings
| Exact Error String / Symptom | Rank | Root Cause & Fix |
|---|---|---|
gpiozero.exc.BadPinFactory: Unable to load any default pin factory |
1 | Cause: Missing lgpio C-library binding.Fix: Run sudo apt install python3-lgpio. |
SYNC Intent Failed (in Google Cloud Logs) |
2 | Cause: Webhook returned non-200 HTTP or malformed JSON. Fix: Validate your SYNC JSON response against the Google Smart Home Fulfillment Docs. |
401 Unauthorized (in Pi FastAPI logs) |
3 | Cause: Bearer token mismatch. Fix: Ensure the token in the Google Action Console matches EXPECTED_TOKEN in the Python script exactly. |
Extending or Simplifying the Build
Depending on your project scope, you may need to pivot from this bare-metal webhook approach. Here is the concrete path forward based on your end goal:
- To Simplify (The Hub Route): If managing Google Cloud Console JSON schemas feels like overkill, abandon the custom webhook. Install Home Assistant on a Pi 4 or Pi 5. Home Assistant handles the Google Home OAuth handshake natively via the "Google Assistant" integration, and you can use the native
rpi_gpiointegration to toggle pins. Trade-off: Requires a heavier, more expensive Pi and a 2GB+ RAM footprint. - To Extend (The Fleet Route): If you need to control relays in other rooms where the Pi Zero's WiFi signal won't reach, keep the Pi Zero 2 W as the central webhook receiver, but install Mosquitto MQTT on it. Modify the FastAPI EXECUTE block to publish MQTT messages instead of toggling local GPIO. Deploy cheap ESP32-WROOM-32 nodes in other rooms to subscribe to those MQTT topics and drive local relays. This scales your Google Home integration to dozens of rooms without latency penalties.
For further reading on modern GPIO handling, refer to the official gpiozero documentation, and for securing your local endpoints, review the Cloudflare Zero Trust Tunnel guides.






