nestgraph2 is a Python collector that streams Nest thermostat state over the Nest Observe gRPC protocol, decodes protobuf payloads into structured device metrics, and writes time-series data to InfluxDB v2. There are importable Grafana dashboards included to visualize the data.
It is designed to run as a long-lived daemon (Docker or local Python) with an event-driven Observe stream, protobuf-backed trait parsing, and lightweight scheduled jobs for daily thermal metrics used by Grafana dashboards.
Sadly Google's Smart Device Management (SDM) API didn't have all the staging information I was after. Instead, this project reverse engineered the necessary protobufs and uses gRPC to maintain get the same data stream as home.nest.com. Likely some AI slop is still lingering here as I haven't gotten around to cleaning up all the cruft from my first vibe project. The interfaces could be extended to control the system, but because of the binary packed nature of the protobufs I didn't want an issues with unexpected commands so this project is data collection only.
Use this sequence for a clean first-time setup.
Quick links:
- 1) Prerequisites
- 2) Create Runtime Config
- 3) Capture Nest Auth And Create config-tokens-json
- 4) Docker Only Create .env
- 5) Start The Collector
- 6) Validate First Run
- 7) Import Dashboards Into Grafana
- Git clone of this repository.
- Python 3.12+ (for local runs), or Docker Desktop with Compose (for container runs).
- InfluxDB 2.x instance and API token with write access to your target bucket root.
- Nest account access so you can capture a fresh HAR and extract tokens.
Copy the example file and create your local runtime config:
Copy-Item config\config.json.example config\config.jsonMinimum required keys in config/config.json:
influxdb.urlinfluxdb.tokeninfluxdb.orginfluxdb.bucket
Commonly required weather keys:
weather.latweather.lon
Important: config/config.json is gitignored.
The collector startup path is token/session based. Capture a HAR from Firefox, then extract tokens.
- Sign in at
https://home.nest.com. - Open DevTools (
F12) and go to Network. - Enable persistent logging and reload the page.
- Wait until auth calls finish (
iframerpcissueToken,issue_jwt, andapp_launch). - Export HAR to this repo.
- Run:
python utilities/ff_har_tokens.py --har "home.nest.com_relogin_main_Archive [26-05-20 14-00-30].har"This writes/merges config/tokens.json.
Optional: if you use encrypted token storage, pass the same key used at runtime. If you skip this, tokens are still encrypted on first collector run when a key is present:
python utilities/ff_har_tokens.py --har "path\to\capture.har" --key "your-passphrase"If you plan to run with Docker Compose, copy .env.example to .env and set values as needed.
Recommended key:
NEST_TOKEN_ENCRYPTION_KEY
Optional overrides:
LIFECHECK_FILELIFECHECK_INTERVAL_SECONDS
When running in Docker Compose, the collector reads the key from /run/secrets/nest_key first,
then falls back to NEST_TOKEN_ENCRYPTION_KEY if no secret file is present.
Choose one path.
Docker path (recommended for long-running deployment):
.\start-collector.ps1
docker compose logs -f collectorCommon startup options:
# Build before start
.\start-collector.ps1 -Build
# Force recreate container on start
.\start-collector.ps1 -ForceRecreate
# Build and force recreate
.\start-collector.ps1 -Build -ForceRecreate
# Force config overwrite from local source folder
.\start-collector.ps1 -ForceConfigSync
# Show script usage/help
.\start-collector.ps1 -hLocal Python path (development):
python -m venv .venv
.venv\Scripts\Activate.ps1
pip install -r requirements.txt
python -m src.mainVerbose decoded Observe snapshots:
python -m src.main --verbose-observe-decodedAfter startup, confirm these checks:
- Collector logs show Observe stream activity (no repeated auth failures).
device_indexpoints are being written to<bucket>_v2_devmap.- Thermostat/weather points are being written to
<bucket>_v2_raw.
If you run with Docker, stop with:
docker compose downCopy the dashboard JSON files from this repo to the machine where you access Grafana:
grafana/metrics.jsongrafana/realtime.json
Then import both dashboards in Grafana UI:
- Open Grafana and go to
Dashboards->New->Import. - Choose
Upload dashboard JSON file. - Import
metrics.json, then importrealtime.json. - Select your InfluxDB data source when prompted.
Validation commands are in the Grafana Dashboards section below.
- Streams Nest Observe updates and writes thermostat state on change.
- Polls outdoor weather (Open-Meteo by default, Nest weather optional).
- Tracks state-duration events (heating/cooling/fan/away and stage flags).
- Computes daily metrics and thermal resistance values.
- Maintains stable device index mapping (
device_id -> dev_idx) in InfluxDB and local cache.
- Main entrypoint:
python -m src.main - Event collector thread:
src.collector_event.EventThermostatCollector - Scheduler: daily metrics compute at 02:00 in configured timezone (
settings.timezone) - Token refresh: background refresh loop in the event collector
- Health heartbeat: lifecheck file touched only when stream activity is fresh
Bucket names are derived from influxdb.bucket in config/config.json:
<root>_v2_raw<root>_v2_model<root>_v2_rollup<root>_v2_event<root>_v2_devmap
Primary measurements currently written by code:
- Raw bucket (
<root>_v2_raw)thermostatindoor_sensorindoor_dynamicsweatherhvac_cycleenergy_dailythermal_resistance
- Model bucket (
<root>_v2_model)computed_metrics
- Event bucket (
<root>_v2_event)state_duration_event
- Devmap bucket (
<root>_v2_devmap)device_index
Collector startup is token/session based and reads auth values from config plus token cache.
Primary flow:
- Follow Step 3 in Getting Started to create or refresh
config/tokens.jsonfrom a HAR capture.
Token cache file path is controlled by connect_nest.token_file (default config/tokens.json).
Extracted fields (when present) include:
issue_tokengoogle_cookie_headergoogle_bearer_tokenaccess_tokentransport_urluserid/user
Note: runtime token cache uses userid in tokens.json. The config override key is nest.user_id.
Custom output path:
python utilities/ff_har_tokens.py --har "path\\to\\capture.har" --out config/tokens.jsonIf token file encryption is enabled, pass the same key used by the collector (or set NEST_TOKEN_ENCRYPTION_KEY):
python utilities/ff_har_tokens.py --har "path\\to\\capture.har" --key "your-passphrase"To decrypt an encrypted tokens file back to plaintext JSON:
python utilities/ff_har_tokens.py --decrypt --out config/tokens.json --key "your-passphrase"Top-level sections expected by code:
nestpostal_code,country_code
influxdburl,token,org,bucket
weatherlat,lon,outdoor_source(open_meteoornest)
connect_nestobserve_timeout_sec,token_refresh_margin_sec,token_file
settingstimezone,temp_units,analog_min_interval_seconds,weather_poll_interval_seconds,mapping_sync_interval_seconds,device_label_short_id_len,log_level
Additional note on defaults:
analog_min_interval_secondsdefaults to60if omitted.weather_poll_interval_secondsdefaults to900if omitted.- The provided
config/config.json.exampleis intentionally minimal; optional runtime keys above can be added as needed.
These keys are supported by code but are typically used for troubleshooting, migrations, or specialized setups.
nestauth override keys:access_token,transport_url,user_id,user,google_bearer_token
Dry run:
python -m src.metrics --cadence-hours 24 --backfill-days 14 --loop-interval-hours 24 --dry-runWrite mode:
python -m src.metrics --cadence-hours 24 --backfill-days 14 --loop-interval-hours 24Optional fallback mode (if state_duration_event data is missing):
python -m src.metrics --cadence-hours 24 --backfill-days 14 --loop-interval-hours 24 --enable-duration-fallbackTargeted date range helper script:
python utilities/backfill_metrics.py --start 2026-01-01 --end 2026-01-31Delete prior backfill measurements before recompute:
python utilities/delete_backfill_measurements.pyDashboards live in grafana/:
grafana/metrics.jsongrafana/realtime.json
Validation script:
scripts/validate-grafana-dashboard.ps1
Examples:
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/validate-grafana-dashboard.ps1
powershell -NoProfile -ExecutionPolicy Bypass -File scripts/validate-grafana-dashboard.ps1 -DashboardPath grafana/realtime.jsonSee grafana/README.md for task-based validation details.
Compile protobuf bindings:
- Windows:
scripts\compile_protos.bat - Cross-platform Python:
python scripts/compile_protos.py - Linux/macOS/WSL:
bash scripts/compile_protos.sh
Additional details:
docs/protobuf_compilation.mddocs/proto_capture.md
nestgraph2/
|-- config/
| |-- config.json.example
|-- docs/
| |-- protobuf_compilation.md
| |-- proto_capture.md
|-- grafana/
| |-- README.md
| |-- metrics.json
| |-- realtime.json
| `-- schema/
|-- nest-observe/ # git submodule
|-- proto/
| |-- generated/
|-- scripts/
| |-- compile_protos.bat
| |-- compile_protos.py
| |-- compile_protos.sh
| `-- validate-grafana-dashboard.ps1
|-- src/
| |-- main.py
| |-- collector.py
| |-- collector_event.py
| |-- grpc_web.py
| |-- nest_client.py
| |-- metrics.py
| |-- influx_writer.py
| |-- weather_client.py
| `-- token_store.py
|-- tests/
|-- utilities/
|-- docker-compose.yml
|-- Dockerfile
`-- requirements.txt
- Config not found:
- Set
CONFIG_PATHor createconfig/config.jsonfrom example.
- Set
- No devices discovered in metrics run:
- Ensure
device_indexhas been populated by running collector first.
- Ensure
- Container unhealthy:
- Check observe stream logs and lifecheck freshness.
- Weather fields are null:
- Verify
weather.lat/lonfor Open-Meteo, ornest.postal_codefor Nest weather mode.
- Verify

