# Authentication Source: https://docs.nolongerevil.com/api-reference/authentication Authentication differences between the hosted service and self-hosted server ## Overview Authentication works differently depending on whether you use the hosted service or the self-hosted server. | | Hosted Service | Self-Hosted | | ------------------- | ------------------------------ | ---------------------------- | | **Control API** | `nle_` Bearer token (required) | No auth by default | | **Device Protocol** | HTTP Basic (device firmware) | HTTP Basic (device firmware) | *** ## Self-Hosted: Control API (Port 8082) The self-hosted control API (port 8082) has **no authentication by default**. Any client on your network can call it without credentials. By design, the control API is intended for use on a trusted local network. If you expose port 8082 to the public internet, you should place it behind a reverse proxy (nginx, Caddy, Traefik) that handles authentication. ```bash theme={null} # No Authorization header needed curl -X POST http://your-server:8082/command \ -H "Content-Type: application/json" \ -d '{"serial": "02AB01AC012345678", "command": "set_temperature", "value": 21}' ``` ### Optional: API Key Auth (via Reverse Proxy) If you configure a reverse proxy with HTTP Basic Auth or a Bearer token header, the control API passes those headers through unchanged. The Python server itself does not validate them. *** ## Self-Hosted: Device Protocol (Port 8000) Nest thermostats use **HTTP Basic Auth** on every request to the device protocol endpoints. The server extracts the device serial from the credentials — it does **not** validate passwords. ``` Authorization: Basic ``` The user ID follows the format `d.{SERIAL}.{suffix}`: ``` d.09AA01AB12345678.BC7C9039 → serial = 09AA01AB12345678 ``` The server accepts all credentials. This is intentional — there is no credential provisioning in the self-hosted deployment. The serial extracted from Basic Auth is used for device identification only. *** ## Hosted Service: API Key Authentication API key authentication (`nle_` prefixed keys) applies to the **hosted service at nolongerevil.com only**. Self-hosted deployments do not use this system. ### Getting a Hosted API Key 1. Go to [https://nolongerevil.com/settings](https://nolongerevil.com/settings) 2. Click the **API Keys** tab 3. Click **Generate New Key** 4. Select scopes: `read` and/or `write` 5. Copy the key immediately — it's shown only once ### API Key Format ``` nle_012e7ffdd4ac7b83848c849c8417d8b632f076c2c10e63ebf69aae3f16b9a914 ``` ### Using a Hosted API Key Include the key as a Bearer token: ```bash cURL theme={null} curl https://nolongerevil.com/api/v1/devices \ -H "Authorization: Bearer nle_your_api_key_here" ``` ```javascript JavaScript theme={null} const response = await fetch('https://nolongerevil.com/api/v1/devices', { headers: { 'Authorization': 'Bearer nle_your_api_key_here' } }); ``` ```python Python theme={null} import requests response = requests.get( 'https://nolongerevil.com/api/v1/devices', headers={'Authorization': 'Bearer nle_your_api_key_here'} ) ``` ### API Key Scopes | Scope | Description | | ------- | ---------------------------------------------- | | `read` | View device status and settings | | `write` | Control devices (temperature, mode, fan, etc.) | Keys are hashed using SHA-256 before storage. The plaintext key is never stored — only the hash. Immediately revoke it in Settings → API Keys, then generate a new key and update your applications. *** ## Summary | Scenario | Auth Method | | ---------------------------------------------------- | ----------------------------------------------- | | Calling the self-hosted control API from a script | None required | | Thermostat connecting to self-hosted device protocol | HTTP Basic (serial extracted, password ignored) | | Calling the hosted service API | `Authorization: Bearer nle_...` | | Thermostat connecting to hosted service | HTTP Basic (managed by firmware provisioning) | # POST /command Source: https://docs.nolongerevil.com/api-reference/control/command Send a control command to a thermostat ## Overview The `/command` endpoint lets you control thermostat settings programmatically from the Control API (port 8082). ## Endpoint ``` POST http://your-server:8082/command ``` This endpoint has **no authentication** by default. Secure it with a reverse proxy if exposing publicly. ## Request ### Headers ```http theme={null} POST /command HTTP/1.1 Host: your-server:8082 Content-Type: application/json ``` ### Body ```json theme={null} { "serial": "02AB01AC012345678", "command": "set_temperature", "value": 21.5 } ``` ### Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | --------------------------------------- | | `serial` | string | Yes | Device serial number | | `command` | string | Yes | Command name (see below) | | `value` | varies | Yes | Command value (type depends on command) | *** ## Supported Commands ### `set_temperature` Set the target temperature. Routes to the `shared` bucket. | Value type | Description | Example | | --------------------------------- | ------------------------ | ------------------------- | | `number` | Single temperature in °C | `21.5` | | `{"high": number, "low": number}` | Range mode (heat-cool) | `{"high": 24, "low": 18}` | ```json theme={null} {"serial": "02AB01AC012345678", "command": "set_temperature", "value": 21.5} ``` ```json theme={null} {"serial": "02AB01AC012345678", "command": "set_temperature", "value": {"high": 24, "low": 18}} ``` Temperatures are always in Celsius (4.5–32°C). The thermostat displays them in the user's preferred scale. Temperature changes also set `target_change_pending: true` to wake the display. *** ### `set_mode` Set the HVAC mode. Routes to the `shared` bucket. | Value | Description | | ------------- | ----------------------------------------- | | `"off"` | System off | | `"heat"` | Heating only | | `"cool"` | Cooling only | | `"heat-cool"` | Auto (maintains temperature range) | | `"emergency"` | Emergency heat (requires `has_emer_heat`) | ```json theme={null} {"serial": "02AB01AC012345678", "command": "set_mode", "value": "heat"} ``` The server checks device capabilities before applying the mode. Requesting `heat` on a cooling-only device returns a 400 error. `"eco"` is not a valid mode here — use `set_away` instead to engage eco mode. *** ### `set_away` Enable or disable eco (away) mode via `manual_eco_all` in the structure bucket. | Value | Description | | ------- | -------------------------- | | `true` | Enable eco/away mode | | `false` | Return to normal operation | ```json theme={null} {"serial": "02AB01AC012345678", "command": "set_away", "value": true} ``` This uses `manual_eco_all` rather than the `away` field, because the firmware's schedule preconditioning can revert `away`-triggered eco but respects manual eco. A `manual_eco_timestamp` is automatically set to the current time. *** ### `set_fan` Control the fan. Routes to the `device` bucket. Requires `has_fan: true`. | Value | Description | | -------- | --------------------------------------------------------- | | `"on"` | Run fan using stored duration preference (default 60 min) | | `"auto"` | Turn off fan timer (auto mode) | | `number` | Run fan for this many seconds | ```json theme={null} {"serial": "02AB01AC012345678", "command": "set_fan", "value": "on"} ``` ```json theme={null} {"serial": "02AB01AC012345678", "command": "set_fan", "value": 1800} ``` *** ### `set_eco_temperatures` Set the eco mode (away) temperature bounds. Routes to the `device` bucket. ```json theme={null} { "serial": "02AB01AC012345678", "command": "set_eco_temperatures", "value": {"high": 26.0, "low": 15.5} } ``` | Field | Description | | ------ | --------------------------- | | `high` | Upper eco temperature in °C | | `low` | Lower eco temperature in °C | *** ### `set_schedule` Replace the full weekly schedule. Routes to the `schedule` bucket (full replacement, not merge). ```json theme={null} { "serial": "02AB01AC012345678", "command": "set_schedule", "value": { "ver": 2, "schedule_mode": "HEAT", "days": { "0": [{"type": "HEAT", "time": 25200, "temp": 20.0}], "1": [{"type": "HEAT", "time": 25200, "temp": 20.0}] } } } ``` Schedule format requirements: * `ver` must be `2` * `schedule_mode`: `"HEAT"`, `"COOL"`, or `"RANGE"` * Day keys: `"0"` (Monday) through `"6"` (Sunday) * `time`: seconds from midnight (0–86399) * `type`: `"HEAT"`, `"COOL"`, or `"RANGE"` * `RANGE` type requires `"temp-min"` and `"temp-max"` instead of `"temp"` *** ### `set_schedule_mode` Change the active schedule mode without replacing the schedule. Routes to the `shared` bucket. | Value | Description | | --------- | ------------------------ | | `"HEAT"` | Heat schedule | | `"COOL"` | Cool schedule | | `"RANGE"` | Heat-cool range schedule | ```json theme={null} {"serial": "02AB01AC012345678", "command": "set_schedule_mode", "value": "HEAT"} ``` *** ### `set_device_setting` Set one or more cloud-writable device bucket fields. Routes to the `device` bucket. Only fields from the whitelist are accepted. ```json theme={null} { "serial": "02AB01AC012345678", "command": "set_device_setting", "value": { "temperature_scale": "F", "learning_mode": true } } ``` | Category | Fields | | ---------------- | ---------------------------------------------------------------------------------------------------------------------------- | | Safety | `lower_safety_temp_enabled`, `upper_safety_temp_enabled`, `lower_safety_temp`, `upper_safety_temp` | | Temperature lock | `temp_lock_on`, `temp_lock_pin_hash`, `temp_lock_high_temp`, `temp_lock_low_temp` | | Learning | `learning_mode`, `preconditioning_enabled`, `preconditioning_active` | | Humidity | `target_humidity_enabled`, `target_humidity` | | Display | `temperature_scale`, `time_to_target`, `time_to_target_training_status` | | Sunblock | `sunlight_correction_enabled` | | Fan | `fan_timer_duration_minutes`, `fan_duty_cycle`, `fan_duty_start_time`, `fan_duty_end_time`, `fan_schedule_speed` | | Heat pump | `heat_pump_aux_threshold_enabled`, `heat_pump_aux_threshold`, `heat_pump_comp_threshold_enabled`, `heat_pump_comp_threshold` | | Wiring | `equipment_type`, `heat_source` | | Hot water | `hot_water_boost_time_to_end`, `hot_water_active` | | Filter | `filter_reminder_enabled`, `filter_reminder_level` | | Locale | `postal_code`, `country_code` | *** ## Response ### Success (200 OK) ```json theme={null} { "success": true, "data": { "object_key": "shared.02AB01AC012345678", "values": { "target_temperature": 21.5, "target_change_pending": true } } } ``` ### Error (400 Bad Request) ```json theme={null} { "success": false, "message": "Unknown command: set_foo" } ``` ```json theme={null} { "success": false, "message": "Device does not support heating (can_heat=false)" } ``` ### Error (500 Internal Server Error) ```json theme={null} { "success": false, "message": "..." } ``` *** ## Examples ```bash Set Temperature theme={null} curl -X POST http://your-server:8082/command \ -H "Content-Type: application/json" \ -d '{"serial": "02AB01AC012345678", "command": "set_temperature", "value": 21.5}' ``` ```bash Set Mode to Heat theme={null} curl -X POST http://your-server:8082/command \ -H "Content-Type: application/json" \ -d '{"serial": "02AB01AC012345678", "command": "set_mode", "value": "heat"}' ``` ```bash Enable Eco Mode theme={null} curl -X POST http://your-server:8082/command \ -H "Content-Type: application/json" \ -d '{"serial": "02AB01AC012345678", "command": "set_away", "value": true}' ``` ```python Python theme={null} import requests def send_command(serial, command, value): resp = requests.post('http://your-server:8082/command', json={ 'serial': serial, 'command': command, 'value': value }) return resp.json() # Set temperature to 21°C send_command('02AB01AC012345678', 'set_temperature', 21.0) # Switch to cool mode send_command('02AB01AC012345678', 'set_mode', 'cool') ``` *** ## Next Steps Query current device state List all connected devices # POST /api/configure-nest Source: https://docs.nolongerevil.com/api-reference/control/configure-nest Point a discovered Nest device at this server ## Overview Updates a Nest thermostat's `cloudregisterurl` setting to point at this NLE server. After this call, the device will connect to this server on its next boot or reconnect. This is the second step in the device setup flow, after using `/api/scan-network` to discover devices. ## Endpoint ``` POST http://your-server:8082/api/configure-nest ``` ## Request ### First Attempt (no API key) ```json theme={null} { "ip": "192.168.1.55" } ``` ### Retry With API Key (if first attempt returns 401) ```json theme={null} { "ip": "192.168.1.55", "api_key": "02AB01AC012345678" } ``` | Field | Type | Required | Description | | --------- | ------ | -------- | ------------------------------------------------------ | | `ip` | string | Yes | IP address of the Nest device to configure | | `api_key` | string | No | Device serial/API key, required if device demands auth | ## Response ### Success (200 OK) ```json theme={null} { "success": true, "device_name": "Nest Thermostat" } ``` ### Authentication Required (401) ```json theme={null} { "success": false, "auth_required": true } ``` Retry the request with `"api_key": "{device_serial}"` to authenticate. ### Error (4xx/5xx) ```json theme={null} { "success": false, "error": "Connection failed: ..." } ``` | Status | Meaning | | ------ | ----------------------------------------------------- | | `401` | Device requires authentication — retry with `api_key` | | `502` | Could not connect to the device | ## What It Sets The server sends a POST to the device's local API (`http://{ip}:8080/cgi-bin/api/settings`) with: ```json theme={null} { "endpoint": "http://your-server:8000" } ``` This updates the device's `cloudregisterurl`, which is the URL it uses for all cloud communication. ## Examples ```bash cURL (two-step) theme={null} # Step 1: try without key curl -X POST http://your-server:8082/api/configure-nest \ -H "Content-Type: application/json" \ -d '{"ip": "192.168.1.55"}' # If auth_required: true, step 2: retry with serial curl -X POST http://your-server:8082/api/configure-nest \ -H "Content-Type: application/json" \ -d '{"ip": "192.168.1.55", "api_key": "09AA01AB12345678"}' ``` ```python Python theme={null} import requests def configure_nest(ip, api_key=None): payload = {'ip': ip} if api_key: payload['api_key'] = api_key resp = requests.post('http://your-server:8082/api/configure-nest', json=payload) result = resp.json() if resp.status_code == 401 and result.get('auth_required') and not api_key: # Retry with the device serial as API key serial = input(f"Enter serial for device at {ip}: ") return configure_nest(ip, api_key=serial) return result result = configure_nest('192.168.1.55') print(result) ``` ## Related Discover devices on the network first Register a device after pointing it here # Control API Overview Source: https://docs.nolongerevil.com/api-reference/control/control-overview Self-hosted control API on port 8082 — commands, status, and device management ## Overview The Control API is the interface you use to control your thermostats, query device status, and manage registrations. It runs on **port 8082** and has **no authentication by default**. Port 8082 is separate from port 8000 (the device protocol port). Thermostats connect to port 8000. You connect to port 8082. ## Base URL ``` http://your-server:8082 ``` Replace `your-server` with your server's IP address or hostname. ## Authentication The Control API has **no authentication by default**. It is designed for use on a trusted local network. If you expose it to the public internet, put it behind a reverse proxy with authentication. ## Available Endpoints ### Thermostat Control | Method | Endpoint | Description | | ------ | ---------------- | ---------------------------------------------- | | `POST` | `/command` | Send a command to a thermostat | | `POST` | `/notify-device` | Force-push current state to device subscribers | ### Status & Monitoring | Method | Endpoint | Description | | ------ | --------------- | ---------------------------------- | | `GET` | `/status` | Get full device state | | `GET` | `/api/events` | SSE stream of device state changes | | `GET` | `/api/schedule` | Get the current weekly schedule | | `GET` | `/api/stats` | Server statistics | | `GET` | `/health` | Server health check | ### Device Management | Method | Endpoint | Description | | ------ | ------------------------------- | ------------------------------------- | | `GET` | `/api/devices` | List all registered devices | | `POST` | `/api/register` | Claim entry key and register a device | | `POST` | `/api/dismiss-pairing/{serial}` | Dismiss pairing dialog on device | ### Network Discovery | Method | Endpoint | Description | | ------ | --------------------- | --------------------------------------------- | | `POST` | `/api/scan-network` | Scan subnet for Nest devices | | `POST` | `/api/configure-nest` | Point a discovered Nest device at this server | ### Configuration | Method | Endpoint | Description | | ------ | ------------------ | ---------------------------------------- | | `GET` | `/api/config` | Server configuration (for the dashboard) | | `POST` | `/api/mqtt-config` | Configure MQTT integration | ## Request Format All requests that have a body use `Content-Type: application/json`. ## Response Format Success responses always return `200 OK` with a JSON body. Error responses use appropriate 4xx/5xx status codes. ```json theme={null} // Success {"success": true, "data": {...}} // Error {"success": false, "message": "Device not found"} // Some endpoints return data directly (not wrapped in success/data) {"serial": "...", "current_temperature": 21.5, ...} ``` ## Port Configuration The control API port is configured via the `CONTROL_PORT` environment variable (default: `8082`). The bind address is configured via `CONTROL_HOST` (default: `0.0.0.0`). Send thermostat commands Query device state # GET /api/devices Source: https://docs.nolongerevil.com/api-reference/control/devices List all registered devices and their current state ## Overview Returns a list of all devices known to the server, with full status for each. When `require_device_pairing` is enabled (see environment config), only devices that have completed the pairing/registration flow are returned. Otherwise, all devices the server has seen are returned. ## Endpoint ``` GET http://your-server:8082/api/devices ``` ## Response ### Success (200 OK) ```json theme={null} { "devices": [ { "serial": "02AB01AC012345678", "api_key": "02AB01AC012345678", "is_available": true, "last_seen": "2026-04-01T12:00:00", "name": "Living Room", "current_temperature": 20.5, "target_temperature": 21.0, "mode": "heat", "hvac": { "heater": true, "ac": false, "fan": false }, "capabilities": { "can_heat": true, "can_cool": false, "has_fan": false, "has_emer_heat": false, "has_humidifier": false, "has_dehumidifier": false }, "eco_mode": "schedule", "subscription_count": 1, "away": false, "schedule_mode": "HEAT" } ], "total": 1 } ``` ### Response Fields | Field | Type | Description | | ------------------------------ | ------ | -------------------------------------- | | `devices` | array | Array of device status objects | | `total` | number | Total device count | | `devices[].subscription_count` | number | Number of active subscribe connections | Each device object contains the same fields as [`GET /status`](/api-reference/control/status). ## Examples ```bash cURL theme={null} curl http://your-server:8082/api/devices ``` ```python Python theme={null} import requests resp = requests.get('http://your-server:8082/api/devices') data = resp.json() for device in data['devices']: available = "online" if device['is_available'] else "offline" print(f"{device['serial']} ({device.get('name', 'unnamed')}) — {available}") print(f" Temp: {device['current_temperature']}°C → {device['target_temperature']}°C") ``` ```javascript JavaScript theme={null} const resp = await fetch('http://your-server:8082/api/devices'); const { devices } = await resp.json(); devices.forEach(d => { console.log(`${d.serial}: ${d.current_temperature}°C, mode=${d.mode}`); }); ``` ## Related Full status for a single device Register a new device # GET /api/events Source: https://docs.nolongerevil.com/api-reference/control/events Server-Sent Events stream of device state changes ## Overview `/api/events` is a **Server-Sent Events (SSE)** endpoint that streams a lightweight notification whenever any device's state changes. Use it to keep a UI or integration updated without polling `/status` repeatedly. ## Endpoint ``` GET http://your-server:8082/api/events ``` ## Response The endpoint sends an SSE stream. Each event has: ``` data: {"serial": "02AB01AC012345678"} data: {"serial": "02AB01AC012345678"} ``` The event payload only contains the serial number of the device that changed. To get the new state, follow up with a `GET /status?serial={serial}` call. ### Response Headers ```http theme={null} Content-Type: text/event-stream Cache-Control: no-cache X-Accel-Buffering: no ``` The `X-Accel-Buffering: no` header is included for nginx compatibility. Without it, nginx may buffer the stream and delay delivery of events. ## Examples ```javascript JavaScript (EventSource) theme={null} const source = new EventSource('http://your-server:8082/api/events'); source.onmessage = async (event) => { const { serial } = JSON.parse(event.data); console.log(`Device changed: ${serial}`); // Fetch updated status const resp = await fetch(`http://your-server:8082/status?serial=${serial}`); const status = await resp.json(); console.log(`New temperature: ${status.current_temperature}°C`); }; source.onerror = (err) => { console.error('SSE connection error', err); }; ``` ```python Python (sseclient) theme={null} import sseclient import requests import json resp = requests.get('http://your-server:8082/api/events', stream=True) client = sseclient.SSEClient(resp) for event in client.events(): data = json.loads(event.data) print(f"Device changed: {data['serial']}") ``` ```bash cURL theme={null} curl -N http://your-server:8082/api/events ``` ## Behavior * The connection stays open indefinitely until the client disconnects. * Events are emitted whenever a device state change is processed (subscribe PUT, command execution). * There is no initial burst of events on connection — only future changes are sent. * If the server restarts, the SSE connection will drop and the client must reconnect. ## Use Case: Live Dashboard ```javascript theme={null} // Auto-reconnecting SSE client function connectEvents(onDeviceChange) { let source = null; function connect() { source = new EventSource('http://your-server:8082/api/events'); source.onmessage = async (e) => { const { serial } = JSON.parse(e.data); const resp = await fetch(`http://your-server:8082/status?serial=${serial}`); onDeviceChange(await resp.json()); }; source.onerror = () => { source.close(); setTimeout(connect, 5000); // reconnect after 5s }; } connect(); return () => source?.close(); } ``` ## Related Get full device state after an event Manually trigger a push to subscribers # GET /health Source: https://docs.nolongerevil.com/api-reference/control/health Server health check ## Overview Simple health check endpoint. Returns `{"status": "ok"}` if the server is running. Useful for load balancers, container orchestration, and monitoring tools. ## Endpoint ``` GET http://your-server:8082/health ``` ## Response ### Success (200 OK) ```json theme={null} { "status": "ok" } ``` The server always returns `200 OK` with this body as long as the process is running. There is no deeper health check (e.g., database connectivity). ## Examples ```bash cURL theme={null} curl http://your-server:8082/health ``` ```bash Docker HEALTHCHECK theme={null} HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ CMD curl -f http://localhost:8082/health || exit 1 ``` ```yaml Home Assistant (binary sensor via RESTful) theme={null} binary_sensor: - platform: rest name: "NLE Server" resource: http://your-server:8082/health value_template: "{{ value_json.status == 'ok' }}" device_class: connectivity scan_interval: 60 ``` ## Related Detailed server statistics Device protocol health check (port 8000) # POST /notify-device Source: https://docs.nolongerevil.com/api-reference/control/notify Force-push current state to all device subscribers ## Overview Sends all currently stored state objects for a device to any active subscribe connections. This is useful for testing, debugging, or forcing an immediate state refresh on the thermostat without waiting for the normal subscribe cycle. ## Endpoint ``` POST http://your-server:8082/notify-device ``` ## Request ```json theme={null} { "serial": "02AB01AC012345678" } ``` | Field | Type | Required | Description | | -------- | ------ | -------- | -------------------- | | `serial` | string | Yes | Device serial number | ## Response ### Success (200 OK) ```json theme={null} { "success": true, "subscribers_notified": 1 } ``` | Field | Type | Description | | ---------------------- | ------- | ------------------------------------------------------------- | | `success` | boolean | Always `true` on success | | `subscribers_notified` | number | Number of active subscribe connections that received the push | A `subscribers_notified` of `0` means the device has no active subscribe connections at this moment (it may be sleeping or disconnected). ### Error (400 Bad Request) ```json theme={null} {"error": "Serial required"} ``` ### Error (404 Not Found) ```json theme={null} {"error": "Device not found"} ``` ## When to Use * **Debugging**: Force an immediate state push to see if the device picks up server-side changes * **Testing**: Trigger a state delivery without modifying any values * **Recovery**: If a device seems stuck, this may help it re-sync Normal thermostat control (via `/command`) automatically notifies subscribers. You only need to call this endpoint for explicit manual pushes. ## Example ```bash theme={null} curl -X POST http://your-server:8082/notify-device \ -H "Content-Type: application/json" \ -d '{"serial": "02AB01AC012345678"}' ``` ## Related Send a command (automatically notifies) Subscribe to state change events # POST /api/register Source: https://docs.nolongerevil.com/api-reference/control/register Claim an entry key and register a device to a user ## Overview Completes the device pairing flow by claiming an entry key (the 7-character code displayed on the thermostat) and associating the device with a user. After a successful registration: 1. The entry key is marked as claimed 2. An ownership record is created linking the device to the user 3. The `user` bucket and `structure` bucket are pushed to the device, completing the pairing dialog on-screen ## Endpoint ``` POST http://your-server:8082/api/register ``` ## Request ```json theme={null} { "code": "A3XR7M2", "userId": "homeassistant" } ``` | Field | Type | Required | Description | | -------- | ------ | -------- | -------------------------------------------------------------- | | `code` | string | Yes | 7-character alphanumeric entry code from the thermostat screen | | `userId` | string | Yes | User ID to register the device to (default: `"homeassistant"`) | The `code` field is case-insensitive and automatically uppercased. ## Response ### Success (200 OK) ```json theme={null} { "success": true, "serial": "02AB01AC012345678", "message": "Device 02AB01AC012345678 registered to homeassistant" } ``` ### Failure — Invalid/Expired/Already Claimed (200 OK) ```json theme={null} { "success": false, "message": "Invalid, expired, or already claimed entry key" } ``` Registration failures return `200 OK` (not 4xx) to match the protocol convention. Check the `success` field. ### Error (400 Bad Request) ```json theme={null} { "success": false, "message": "Invalid entry code format. Must be exactly 7 alphanumeric characters." } ``` ```json theme={null} { "success": false, "message": "Missing required fields: code, userId" } ``` ## Pairing Completion After registration, the server pushes two buckets to the device through any held subscribe connection: 1. **`user.{userId}`** bucket with `{"name": "{userId}"}` — this is what triggers the pairing screen to dismiss on the device 2. **`structure.{structureId}`** bucket with `{"name": "Home", "devices": [serial]}` — establishes the home association If the device has no active subscribe connection at registration time, the buckets are stored and will be sent on the next subscribe. ## Examples ```bash cURL theme={null} curl -X POST http://your-server:8082/api/register \ -H "Content-Type: application/json" \ -d '{"code": "A3XR7M2", "userId": "homeassistant"}' ``` ```python Python theme={null} import requests resp = requests.post('http://your-server:8082/api/register', json={ 'code': 'A3XR7M2', 'userId': 'homeassistant' }) result = resp.json() if result['success']: print(f"Registered device: {result['serial']}") else: print(f"Registration failed: {result['message']}") ``` ## Related Entry key generation on the device List registered devices # POST /api/scan-network Source: https://docs.nolongerevil.com/api-reference/control/scan Scan the local subnet for Nest devices ## Overview Scans the `/24` subnet derived from the server's `api_origin` setting and returns all Nest devices found on port 8080. For each device found, it reports whether the device is already configured to point at this server. ## Endpoint ``` POST http://your-server:8082/api/scan-network ``` No request body needed. ## Response ### Success (200 OK) ```json theme={null} { "devices": [ { "ip": "192.168.1.42", "device_name": "Nest Thermostat", "cloudregisterurl": "http://192.168.1.100:8000", "configured": true }, { "ip": "192.168.1.55", "device_name": "Nest Thermostat", "cloudregisterurl": "https://home.nest.com", "configured": false } ], "subnet": "192.168.1.0/24" } ``` ### Response Fields | Field | Type | Description | | ---------------------------- | ------- | -------------------------------------------------- | | `devices` | array | Found Nest devices | | `subnet` | string | The subnet that was scanned | | `devices[].ip` | string | Device IP address | | `devices[].device_name` | string | Device name as configured on the thermostat | | `devices[].cloudregisterurl` | string | Current server URL the device is pointed at | | `devices[].configured` | boolean | `true` if device is already pointed at this server | ### Error (400 Bad Request) ```json theme={null} {"error": "Cannot derive subnet: api_origin has no hostname"} ``` ```json theme={null} {"error": "Cannot resolve 'my-server' to an IP address. Set api_origin to use an IP address instead of a hostname."} ``` ## How It Works The scan: 1. Derives the subnet from your server's `API_ORIGIN` setting (e.g., `http://192.168.1.100:8000` → scan `192.168.1.0/24`) 2. Concurrently probes all 254 hosts for a Nest local API on port 8080 (`GET /cgi-bin/api/settings`) 3. Returns devices that responded, comparing their `cloudregisterurl` to your server's origin The scan requires `API_ORIGIN` to be set to an IP address (not a hostname), or the hostname must be DNS-resolvable from the server. If `API_ORIGIN` uses a domain name that can't be resolved, the scan will fail with an error. ## Typical Workflow ```mermaid theme={null} sequenceDiagram participant U as You participant S as Server (port 8082) participant N as Nest Devices U->>S: POST /api/scan-network S->>N: GET /cgi-bin/api/settings (all 254 IPs in /24) N-->>S: Settings responses S-->>U: {devices: [...], subnet: "192.168.1.0/24"} U->>S: POST /api/configure-nest {ip: "192.168.1.55"} S->>N: POST /cgi-bin/api/settings {endpoint: "http://192.168.1.100:8000"} N-->>S: {device_name: "Nest Thermostat"} S-->>U: {success: true, device_name: "Nest Thermostat"} ``` ## Example ```bash theme={null} curl -X POST http://your-server:8082/api/scan-network ``` ## Related Point a discovered device at this server Register a device after configuring it # GET /api/schedule Source: https://docs.nolongerevil.com/api-reference/control/schedule Get the current weekly schedule for a device ## Overview Returns the device's stored weekly temperature schedule. ## Endpoint ``` GET http://your-server:8082/api/schedule?serial={serial} ``` ## Query Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | -------------------- | | `serial` | string | Yes | Device serial number | ## Response ### Device Has a Schedule (200 OK) ```json theme={null} { "serial": "02AB01AC012345678", "schedule": { "ver": 2, "name": "", "schedule_mode": "HEAT", "days": { "0": [ {"type": "HEAT", "time": 21600, "temp": 18.0, "entry_type": "setpoint"}, {"type": "HEAT", "time": 28800, "temp": 21.0, "entry_type": "setpoint"}, {"type": "HEAT", "time": 64800, "temp": 19.5, "entry_type": "setpoint"} ], "1": [ {"type": "HEAT", "time": 25200, "temp": 20.0, "entry_type": "setpoint"} ], "5": [ {"type": "HEAT", "time": 28800, "temp": 21.5, "entry_type": "setpoint"} ], "6": [ {"type": "HEAT", "time": 28800, "temp": 21.5, "entry_type": "setpoint"} ] } }, "object_revision": 12, "object_timestamp": 1743508800000 } ``` ### No Schedule Yet (200 OK) ```json theme={null} { "serial": "02AB01AC012345678", "schedule": null } ``` ### Response Fields | Field | Type | Description | | ------------------------ | -------------- | ---------------------------------------------- | | `serial` | string | Device serial | | `schedule` | object \| null | Schedule object, or null if no schedule stored | | `schedule.ver` | number | Schedule version (always `2`) | | `schedule.schedule_mode` | string | `"HEAT"`, `"COOL"`, or `"RANGE"` | | `schedule.days` | object | Day-keyed setpoints (see below) | | `object_revision` | number | Bucket revision | | `object_timestamp` | number | Last update timestamp (ms) | ### Schedule Day Keys Day keys are strings `"0"` through `"6"`, where **Monday = `"0"` and Sunday = `"6"`**: | Key | Day | | ----- | --------- | | `"0"` | Monday | | `"1"` | Tuesday | | `"2"` | Wednesday | | `"3"` | Thursday | | `"4"` | Friday | | `"5"` | Saturday | | `"6"` | Sunday | Days not present in the `days` object are considered to have no scheduled changes. ### Setpoint Fields | Field | Type | Description | | ------------ | ------ | -------------------------------------------------------- | | `type` | string | `"HEAT"`, `"COOL"`, or `"RANGE"` | | `time` | number | Seconds from midnight (0–86399). E.g., `21600` = 6:00 AM | | `temp` | number | Target temperature in °C (for HEAT/COOL modes) | | `temp-min` | number | Minimum temperature in °C (RANGE mode only) | | `temp-max` | number | Maximum temperature in °C (RANGE mode only) | | `entry_type` | string | `"setpoint"` (informational) | All temperatures in the schedule are in **Celsius**, regardless of the thermostat's display scale. ### Error (400 Bad Request) ```json theme={null} {"error": "Serial parameter required"} ``` ## Example ```bash cURL theme={null} curl "http://your-server:8082/api/schedule?serial=02AB01AC012345678" ``` ```python Python theme={null} import requests resp = requests.get('http://your-server:8082/api/schedule', params={'serial': '02AB01AC012345678'}) data = resp.json() if data['schedule']: for day_key, entries in data['schedule']['days'].items(): days = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'] print(f"{days[int(day_key)]}:") for e in entries: h, m = divmod(e['time'] // 60, 60) print(f" {h:02d}:{m:02d} → {e.get('temp', f\"{e.get('temp-min')}-{e.get('temp-max')}\")}°C") ``` ## Related Push a new schedule to the device Deep dive on schedule format and behavior # GET /status Source: https://docs.nolongerevil.com/api-reference/control/status Get the full state of a specific device ## Overview Returns the complete current state for a single device, including temperatures, HVAC state, capabilities, and availability. ## Endpoint ``` GET http://your-server:8082/status?serial={serial} ``` ## Query Parameters | Parameter | Type | Required | Description | | --------- | ------ | -------- | -------------------- | | `serial` | string | Yes | Device serial number | ## Response ### Success (200 OK) ```json theme={null} { "serial": "02AB01AC012345678", "api_key": "02AB01AC012345678", "is_available": true, "last_seen": "2026-04-01T12:00:00", "name": "Living Room", "current_temperature": 20.5, "target_temperature": 21.0, "target_temperature_high": null, "target_temperature_low": null, "humidity": 45, "target_humidity": null, "target_humidity_enabled": false, "mode": "heat", "hvac": { "heater": true, "heat_x2": false, "heat_x3": false, "ac": false, "cool_x2": false, "cool_x3": false, "fan": false, "aux_heat": false, "emer_heat": false, "alt_heat": false, "humidifier": false, "dehumidifier": false, "auto_dehum": false, "fan_cooling": false }, "fan_timer_active": false, "fan_timer_timeout": 0, "eco_temperatures": { "high": 26.0, "low": 15.5 }, "is_online": true, "has_leaf": false, "software_version": "5.9.3-5", "temperature_scale": "C", "capabilities": { "can_heat": true, "can_cool": false, "has_fan": false, "has_emer_heat": false, "has_humidifier": false, "has_dehumidifier": false }, "eco_mode": "schedule", "time_to_target": 15, "time_to_target_training_status": "trained", "safety_state": null, "safety_temp_activating_hvac": null, "learning_mode": true, "preconditioning_enabled": false, "backplate_temperature": 20.1, "structure_id": "abc123", "away": false, "schedule_mode": "HEAT" } ``` ### Response Fields | Field | Type | Description | | ---------------------------- | --------------- | -------------------------------------------------------------- | | `serial` | string | Device serial number | | `api_key` | string | Device API key (same as serial for self-hosted) | | `is_available` | boolean | Whether the device is currently reachable | | `last_seen` | string \| null | ISO 8601 timestamp of last device activity | | `name` | string | Device display name | | `current_temperature` | number \| null | Ambient temperature in °C | | `target_temperature` | number \| null | Target setpoint in °C (single-mode) | | `target_temperature_high` | number \| null | Target high in °C (heat-cool range mode) | | `target_temperature_low` | number \| null | Target low in °C (heat-cool range mode) | | `humidity` | number \| null | Current relative humidity (%) | | `target_humidity` | number \| null | Target humidity (%) | | `target_humidity_enabled` | boolean | Whether humidity control is active | | `mode` | string \| null | Active HVAC mode (`heat`, `cool`, `range`, `off`, `emergency`) | | `hvac` | object | Current HVAC equipment runtime states | | `hvac.heater` | boolean | Heater running | | `hvac.ac` | boolean | AC running | | `hvac.fan` | boolean | Fan running | | `hvac.aux_heat` | boolean | Auxiliary heat running | | `fan_timer_active` | boolean | Whether fan timer is active | | `fan_timer_timeout` | number | Fan timer expiry (Unix seconds) | | `eco_temperatures` | object | Eco mode temperature bounds | | `eco_temperatures.high` | number \| null | Eco upper bound in °C | | `eco_temperatures.low` | number \| null | Eco lower bound in °C | | `is_online` | boolean | Whether the device self-reports as online | | `has_leaf` | boolean | Nest Leaf indicator | | `software_version` | string \| null | Firmware version string | | `temperature_scale` | string | `"C"` or `"F"` (display preference) | | `capabilities` | object | Equipment capabilities | | `capabilities.can_heat` | boolean | Device has heating wired | | `capabilities.can_cool` | boolean | Device has cooling wired | | `capabilities.has_fan` | boolean | Device has a fan | | `capabilities.has_emer_heat` | boolean | Device has emergency heat | | `eco_mode` | string \| null | Eco mode state (`"schedule"`, `"manual-eco"`, `"auto-eco"`) | | `time_to_target` | number \| null | Minutes until target temperature reached | | `learning_mode` | boolean \| null | Whether learning is enabled | | `backplate_temperature` | number \| null | Backplate sensor temperature in °C | | `structure_id` | string \| null | Structure ID this device belongs to | | `away` | boolean | Away mode flag | | `schedule_mode` | string \| null | Active schedule mode (`"HEAT"`, `"COOL"`, `"RANGE"`) | ### Error (400 Bad Request) ```json theme={null} {"error": "Serial parameter required"} ``` ### Error (404 Not Found) ```json theme={null} {"error": "Device not found"} ``` ## Example ```bash cURL theme={null} curl "http://your-server:8082/status?serial=02AB01AC012345678" ``` ```python Python theme={null} import requests resp = requests.get('http://your-server:8082/status', params={'serial': '02AB01AC012345678'}) status = resp.json() print(f"Temperature: {status['current_temperature']}°C") print(f"Mode: {status['mode']}") print(f"Heating: {status['hvac']['heater']}") ``` ## Next Steps Control the device Subscribe to live state changes # API Introduction Source: https://docs.nolongerevil.com/api-reference/introduction Overview of the No Longer Evil APIs — hosted service and self-hosted server ## Overview No Longer Evil exposes two separate APIs depending on how you run it: `https://nolongerevil.com/api/v1` — REST API for cloud-managed devices. Requires an `nle_` API key. `http://your-server:8082` — Local control API for self-hosted deployments. No auth required by default. *** ## Two-Port Architecture (Self-Hosted) The self-hosted server runs two independent HTTP services on different ports: | Port | Purpose | Used By | | -------- | --------------------------------------------- | -------------------------------- | | **8000** | Device Protocol API — Nest protocol emulation | Thermostats (firmware) | | **8082** | Control API — User/automation-facing control | Dashboard, scripts, integrations | **Port 8000** speaks the Nest Cloud Protocol. Thermostats connect here using long-poll subscribe/PUT requests. **Port 8082** is where you send commands, query status, and manage devices. This is the API you call from scripts, Home Assistant, or any automation tool. ### Self-Hosted Base URLs ``` Device Protocol (thermostats): http://your-server:8000 Control API (you call this): http://your-server:8082 ``` *** ## Hosted Service Base URL ``` https://nolongerevil.com/api/v1 ``` The hosted REST API (`/api/v1`) does **not** exist on the self-hosted server. Self-hosted uses a flat control API on port 8082. *** ## Quick Start ### Self-Hosted: Send a Command No API key needed. Just POST to port 8082: ```bash theme={null} curl -X POST http://your-server:8082/command \ -H "Content-Type: application/json" \ -d '{"serial": "02AB01AC012345678", "command": "set_temperature", "value": 21.5}' ``` ### Self-Hosted: Get Device Status ```bash theme={null} curl http://your-server:8082/status?serial=02AB01AC012345678 ``` ### Hosted Service: List Devices ```bash theme={null} curl https://nolongerevil.com/api/v1/devices \ -H "Authorization: Bearer nle_your_api_key_here" ``` *** ## Response Format ### Success Response ```json theme={null} { "success": true, "data": { "object_key": "shared.02AB01AC012345678", "values": { "target_temperature": 21.5 } } } ``` ### Error Response ```json theme={null} { "success": false, "message": "Unknown command: set_foo" } ``` *** ## API Sections Commands, status, device management — port 8082 Nest protocol endpoints — port 8000 (advanced/reference) REST API for the hosted service at nolongerevil.com Auth differences between hosted and self-hosted # GET/POST /nest/entry Source: https://docs.nolongerevil.com/api-reference/thermostat/entry Device service discovery — first request a thermostat makes on boot ## Overview `/nest/entry` is the **first request** a Nest thermostat makes when it boots or reconnects to WiFi. The device uses the response to discover the URLs for all other services it needs. This endpoint is part of the **Device Protocol API** on port 8000. It is called directly by thermostat firmware — you do not need to call it yourself. ## Endpoint ``` GET http://your-server:8000/nest/entry POST http://your-server:8000/nest/entry ``` Both methods are supported. Production firmware uses POST with form-urlencoded body. ## Authentication HTTP Basic Auth. The device sends its serial number embedded in the user ID: ``` Authorization: Basic ``` The server extracts the serial from the Basic Auth user ID and accepts all credentials. ## Request ### POST (form-urlencoded — production firmware) ```http theme={null} POST /nest/entry HTTP/1.1 Host: your-server:8000 Content-Type: application/x-www-form-urlencoded Authorization: Basic reset=FALSE&mac=18B430ABCDEF&model=Diamond-2.6&request_id=1&software_version=5.9.3-5 ``` ### POST body fields | Field | Description | | --------------------- | ---------------------------------------------- | | `reset` | `TRUE` if factory reset, `FALSE` otherwise | | `mac` | WiFi MAC address (12 hex chars, no separators) | | `model` | Device model string (e.g., `Diamond-2.6`) | | `request_id` | Monotonically increasing request counter | | `software_version` | Current firmware version | | `wireless_reg_domain` | WiFi regulatory domain (e.g., `US`, `EU`) | | `backplate_model` | Backplate model string | ## Response ### Self-Hosted Response (200 OK) The self-hosted server returns a **flat JSON object** — no wrapper object: ```json theme={null} { "czfe_url": "http://your-server:8000/nest/transport", "transport_url": "http://your-server:8000/nest/transport", "direct_transport_url": "http://your-server:8000/nest/transport", "passphrase_url": "http://your-server:8000/nest/passphrase", "ping_url": "http://your-server:8000/nest/transport", "pro_info_url": "http://your-server:8000/nest/pro_info", "weather_url": "http://your-server:8000/nest/weather/v1?query=", "upload_url": "http://your-server:8000/nest/upload", "software_update_url": "", "server_version": "1.0.1", "tier_name": "local" } ``` ### Self-Hosted Response Fields | Field | Type | Description | | ---------------------- | ------ | ------------------------------------------------ | | `czfe_url` | string | Primary transport URL (same as `transport_url`) | | `transport_url` | string | Long-poll subscribe/PUT endpoint | | `direct_transport_url` | string | Direct transport URL (same server) | | `passphrase_url` | string | Entry key generation endpoint for device pairing | | `ping_url` | string | Connectivity health check endpoint | | `pro_info_url` | string | Pro device info endpoint | | `weather_url` | string | Weather proxy endpoint | | `upload_url` | string | Device log upload endpoint | | `software_update_url` | string | Firmware update URL (empty — no OTA updates) | | `server_version` | string | Server software version | | `tier_name` | string | Always `"local"` for self-hosted | All URLs sent to the device **must include an explicit port number** (e.g., `:8000`). URLs without an explicit port can cause the device to fail TCP keepalive offload (WoWLAN), which is required for low-power operation. The server automatically appends the port via `api_origin_with_port`. ### Hosted Service Response The hosted service returns additional fields: ```json theme={null} { "transport_url": "https://backdoor.nolongerevil.com", "weather_url": "https://backdoor.nolongerevil.com", "czfe_url": "https://backdoor.nolongerevil.com", "direct_transport_url": "https://backdoor.nolongerevil.com", "passphrase_url": "https://backdoor.nolongerevil.com/nest/passphrase", "upload_url": "https://backdoor.nolongerevil.com/nest/upload", "software_update_url": "", "server_version": "1.0.1", "tier_name": "hosted" } ``` ## Device Flow After Entry After receiving the entry response, the thermostat: 1. Stores the `transport_url` 2. Calls `GET {passphrase_url}` to get an entry key (for display during pairing) 3. Begins long-poll subscribing at `POST {transport_url}` (the subscribe endpoint) ```mermaid theme={null} sequenceDiagram participant T as Thermostat participant S as Server (port 8000) T->>S: POST /nest/entry (Basic Auth) S-->>T: {transport_url, passphrase_url, ...} T->>S: GET /nest/passphrase (get entry code to display) S-->>T: {value: "A3XR7M2", expires: 1707...} T->>S: POST /nest/transport (subscribe — long-poll begins) S-->>T: chunked response (headers sent immediately) Note over T,S: Connection held open for server push ``` ## Example ```bash theme={null} curl -X POST http://your-server:8000/nest/entry \ -u "d.02AB01AC012345678.00000000:password" \ -H "Content-Type: application/x-www-form-urlencoded" \ --data-urlencode "reset=FALSE" \ --data-urlencode "model=Diamond-2.6" \ --data-urlencode "software_version=5.9.3-5" ``` ## Next Steps Entry key generation for device pairing Long-poll subscribe endpoint # GET /info Source: https://docs.nolongerevil.com/api-reference/thermostat/info Server provisioning info — used by the firmware installer ## Overview Returns server discovery information used by the firmware installer GUI and provisioning scripts to configure thermostat firmware to point at this server. This endpoint is at the **root level** (`/info`), not under `/nest/`. It is served on **port 8000** (device protocol). ## Endpoint ``` GET http://your-server:8000/info ``` No authentication required. ## Response ### Success (200 OK) ```json theme={null} { "server": "nolongerevil", "version": "1.0.1", "api_origin": "http://192.168.1.100:8000", "cloudregisterurl": "http://192.168.1.100:8000/entry", "ip": "192.168.1.100", "port": 8000, "ssl": false, "require_device_pairing": false, "entry_key_ttl_seconds": 3600 } ``` ### Response Fields | Field | Type | Description | | ------------------------ | ------- | ----------------------------------------------------------------- | | `server` | string | Always `"nolongerevil"` | | `version` | string | Server software version | | `api_origin` | string | Configured `API_ORIGIN` value | | `cloudregisterurl` | string | Value to set in thermostat firmware config (`{api_origin}/entry`) | | `ip` | string | IP address extracted from `api_origin` | | `port` | number | Port extracted from `api_origin` | | `ssl` | boolean | Whether `api_origin` uses HTTPS | | `require_device_pairing` | boolean | Whether entry key pairing is required | | `entry_key_ttl_seconds` | number | How long entry keys remain valid (seconds) | ## Use Case The firmware installer reads this endpoint to get the `cloudregisterurl` value it needs to embed into the thermostat firmware image during flashing. The installer fetches `/info`, extracts `cloudregisterurl`, and writes it into the firmware configuration at flash time. The installer also uses `ip`, `port`, and `ssl` to construct the firmware's network configuration. ## Example ```bash theme={null} curl http://your-server:8000/info | jq . ``` ## Related Control API config (port 8082) Firmware installer setup guide # GET /nest/passphrase Source: https://docs.nolongerevil.com/api-reference/thermostat/passphrase Entry key generation and pairing status for device claiming ## Overview The `/nest/passphrase` endpoint generates 7-character entry keys that allow users to claim (link) devices to their accounts. The device polls this endpoint during pairing to get the code it displays on-screen. A companion endpoint `/nest/passphrase/status` lets the device poll to find out if its entry key has been claimed. These endpoints are part of the **Device Protocol API** on port 8000. They are called directly by thermostat firmware during the pairing flow. ## Endpoints ``` GET http://your-server:8000/nest/passphrase GET http://your-server:8000/nest/passphrase/status ``` *** ## GET /nest/passphrase Returns the current unexpired entry key for the device. If no valid key exists, a new one is generated. The device polls this endpoint repeatedly during pairing. The server returns the **same** key on each request until it's claimed or expires — generating a new key on each call would invalidate the key before the user can enter it. ### Request ```http theme={null} GET /nest/passphrase HTTP/1.1 Host: your-server:8000 Authorization: Basic ``` The device serial is extracted from the Basic Auth user ID. ### Response (200 OK) ```json theme={null} { "value": "A3XR7M2", "expires": 1707234600000 } ``` | Field | Type | Description | | --------- | ---------- | --------------------------------------------------------- | | `value` | string | 7-character alphanumeric entry key (e.g., `A3XR7M2`) | | `expires` | **number** | Expiration timestamp in **milliseconds** since Unix epoch | The `expires` field **must be a JSON number**, not a string. If it is sent as `"1707234600000"` (a string), the device silently rejects the response and never displays the entry key. The expiration must be at least 30 minutes in the future. ### Entry Key Properties * **Length**: 7 alphanumeric characters * **Display format**: The device shows the code as `XXX-XXXX` (e.g., `A3X-R7M2`) * **TTL**: 1 hour by default (configurable via `ENTRY_KEY_TTL_SECONDS`) * **Reuse**: The same key is returned until claimed or expired * **Single-use**: Can only be claimed once ### Error Responses | Status | Body | Cause | | ------ | -------------------------------------------- | --------------------------------- | | `400` | `{"error": "Device serial required"}` | Missing or unparseable Basic Auth | | `503` | `{"error": "Entry key service unavailable"}` | Database unavailable | *** ## GET /nest/passphrase/status Lets the device poll to discover if its entry key has been claimed by a user. ### Request ```http theme={null} GET /nest/passphrase/status HTTP/1.1 Host: your-server:8000 Authorization: Basic ``` ### Response: No Key Found ```json theme={null} { "status": "no_key", "claimed": false, "message": "No entry key found for this device" } ``` ### Response: Key Pending (not yet claimed) ```json theme={null} { "status": "pending", "claimed": false, "expiresAt": 1707234600000 } ``` ### Response: Key Claimed ```json theme={null} { "status": "claimed", "claimed": true, "claimedBy": "homeassistant", "claimedAt": 1707231000000 } ``` | Field | Type | Description | | ----------- | ------- | ----------------------------------------------------- | | `status` | string | `"no_key"`, `"pending"`, or `"claimed"` | | `claimed` | boolean | Whether the key has been claimed | | `claimedBy` | string | User ID that claimed the key (when `claimed: true`) | | `claimedAt` | number | Claim timestamp in ms (when `claimed: true`) | | `expiresAt` | number | Expiration timestamp in ms (when `status: "pending"`) | *** ## Pairing Flow ```mermaid theme={null} sequenceDiagram participant U as User participant T as Thermostat participant S as Server T->>S: GET /nest/passphrase S-->>T: {value: "A3XR7M2", expires: 1707...} T-->>U: Display "A3X-R7M2" loop Poll every ~30s T->>S: GET /nest/passphrase/status S-->>T: {status: "pending", claimed: false} end U->>S: POST /api/register {code: "A3XR7M2", userId: "homeassistant"} S-->>U: {success: true, serial: "09AA01AB12345678"} Note over S: Server pushes user + structure buckets to device T->>S: GET /nest/passphrase/status S-->>T: {status: "claimed", claimed: true, claimedBy: "homeassistant"} Note over T: Pairing screen dismissed ``` ## Next Steps Claim an entry key and register a device Subscribe endpoint (long-poll) # GET /nest/ping Source: https://docs.nolongerevil.com/api-reference/thermostat/ping Device protocol health check ## Overview Simple health check for the device protocol server (port 8000). Returns `{"status": "ok"}` and a timestamp. This is the device protocol health check on **port 8000**. For the control API health check, see [`GET /health`](/api-reference/control/health) on port 8082. ## Endpoint ``` GET http://your-server:8000/nest/ping ``` ## Response ### Success (200 OK) ```json theme={null} { "status": "ok", "timestamp": 1707148800000 } ``` | Field | Type | Description | | ----------- | ------ | ---------------------------------------------------- | | `status` | string | Always `"ok"` | | `timestamp` | number | Current server time in milliseconds since Unix epoch | ## Example ```bash theme={null} curl http://your-server:8000/nest/ping ``` # GET /nest/transport/device/{serial} Source: https://docs.nolongerevil.com/api-reference/thermostat/transport-device List device objects (bucket metadata) for a given serial ## Overview Returns the metadata (revision and timestamp) for all stored bucket objects for a device, without the actual values. Used by the device to discover which buckets the server has state for. This endpoint is part of the **Device Protocol API** on port 8000. It is called directly by thermostat firmware. ## Endpoint ``` GET http://your-server:8000/nest/transport/device/{serial} ``` Also handles legacy path formats: * `/nest/transport/v7/device/device.{serial}` * `/nest/transport/device/device.{serial}` ## Path Parameters | Parameter | Type | Description | | --------- | ------ | -------------------- | | `serial` | string | Device serial number | ## Response ### Success (200 OK) ```json theme={null} { "objects": [ { "object_revision": 42, "object_timestamp": 1707148800000, "object_key": "device.09AA01AB12345678" }, { "object_revision": 15, "object_timestamp": 1707148800000, "object_key": "shared.09AA01AB12345678" }, { "object_revision": 3, "object_timestamp": 1707148800000, "object_key": "schedule.09AA01AB12345678" }, { "object_revision": 1, "object_timestamp": 1707148800000, "object_key": "user.homeassistant" }, { "object_revision": 1, "object_timestamp": 1707148800000, "object_key": "structure.abc123" }, { "object_revision": 1, "object_timestamp": 1707148800000, "object_key": "device_alert_dialog.09AA01AB12345678" } ] } ``` Values are **not included** — only `object_revision`, `object_timestamp`, and `object_key`. The device uses this response to build its initial subscribe request, including only the buckets it has seen before. ### Response Headers ```http theme={null} X-nl-suspend-time-max: 300 X-nl-service-timestamp: 1707148800000 X-nl-defer-device-window: 15 ``` Standard Nest protocol response headers are included. ### Error (400 Bad Request) ```json theme={null} {"error": "Serial required"} ``` ## Related Subscribe endpoint — uses the object list from this endpoint All 28 bucket types documented # POST /nest/transport/put Source: https://docs.nolongerevil.com/api-reference/thermostat/transport-put Device-to-server state updates (PUT) ## Overview `POST /nest/transport/put` is how the thermostat sends local state changes to the server — temperature dial turns, mode changes, sensor readings, HVAC status updates. This endpoint is part of the **Device Protocol API** on port 8000. It is called directly by thermostat firmware — not by you. ## Endpoint ``` POST http://your-server:8000/nest/transport/put ``` ## Request ### Headers ```http theme={null} POST /nest/transport/put HTTP/1.1 Host: your-server:8000 Content-Type: application/json Authorization: Basic X-nl-protocol-version: 1 ``` ### Body (Bucket-Keyed Format) ```http theme={null} { "session": "18b430SERIAL", "shared.09AB1234": { "object_key": "shared.09AB1234", "base_object_revision": 15, "target_temperature": 22.0, "target_temperature_type": "heat" } } ``` ### Body (Objects Array Format) ```json theme={null} { "session": "18b430SERIAL", "objects": [ { "object_key": "shared.09AB1234", "base_object_revision": 15, "value": { "target_temperature": 22.0 } } ] } ``` ### Revision Fields | Field | Used by | Description | | ---------------------- | --------------- | -------------------------------------------------------------- | | `base_object_revision` | Most buckets | Informational — no server validation required | | `if_object_revision` | `shared` bucket | Conditional write guard — server should check current revision | The `shared` bucket uses conditional writes (`if_object_revision`) because both the device and the server may update it concurrently (device dials the temperature, server sends a command at the same time). ## Response ```http theme={null} HTTP/1.1 200 OK Content-Type: application/json ``` ```json theme={null} { "objects": [ { "object_revision": 16, "object_timestamp": 1707149000000, "object_key": "shared.09AB1234" } ] } ``` The response contains the updated revision and timestamp for each bucket that was processed. It does **not** include a `value` field. **Never include a `value` field in the PUT response.** The device treats any `value` in a PUT response as authoritative cloud data and applies every field as an overwrite — including fields the device just updated locally. This can cause the device to silently lose local state changes. ## Server Merge Behavior For each incoming PUT object, the server: 1. Merges the incoming fields into the stored bucket (shallow merge) 2. Only bumps revision and timestamp if values actually changed 3. Does **not** notify subscribers (the PUT response is the only data path back to the device; server pushes go through subscribe) ## Examples A thermostat dial turn triggers a PUT: ```json theme={null} POST /nest/transport/put { "session": "18b430ABCDEF", "shared.09AB01AB12345678": { "object_key": "shared.09AB01AB12345678", "base_object_revision": 15, "if_object_revision": 15, "target_temperature": 22.0, "target_temperature_type": "heat", "target_change_pending": false } } ``` Response: ```json theme={null} { "objects": [ { "object_revision": 16, "object_timestamp": 1707149000000, "object_key": "shared.09AB01AB12345678" } ] } ``` ## Related Subscribe endpoint (server → device) Full protocol documentation # POST /nest/transport Source: https://docs.nolongerevil.com/api-reference/thermostat/transport-subscribe Long-poll subscribe endpoint — core of the Nest device protocol ## Overview `POST /nest/transport` is the heart of the Nest Cloud Protocol. The thermostat maintains a persistent HTTP connection here and receives server-pushed state updates via chunked transfer encoding. This endpoint is part of the **Device Protocol API** on port 8000. It is called exclusively by thermostat firmware — you do not call it directly. ## Endpoint ``` POST http://your-server:8000/nest/transport ``` ## How It Works The subscribe endpoint implements a long-poll pattern: 1. **Device connects** — sends its current bucket revisions/timestamps 2. **Server responds with headers immediately** — `Transfer-Encoding: chunked` is sent right away, allowing the device to offload the open connection to WiFi hardware and sleep 3. **Server holds the connection** — either sends a JSON chunk immediately (if server has newer data) or waits silently 4. **Server sends a chunk** — when state changes, the server serializes the updated buckets and sends them as a chunked response body, waking the device via WoWLAN 5. **Server closes after a batch window** — waits up to 3 seconds after the first chunk for additional data, then closes the connection 6. **Device resubscribes** — on connection close (or after `suspend_time_max` seconds as a safety net), the device reconnects ```mermaid theme={null} sequenceDiagram participant D as Device participant S as Server D->>S: POST /nest/transport (bucket revisions) S-->>D: HTTP/1.1 200 (headers: Transfer-Encoding: chunked) Note over D: Device offloads socket to WiFi, CPU sleeps Note over S: Time passes — command received or timeout S-->>D: {hex_size}\r\n{json_data}\r\n (chunk) Note over D: WoWLAN wakes device CPU S-->>D: 0\r\n\r\n (final chunk — close) D->>S: POST /nest/transport (new revisions — resubscribes) ``` ## Request ### Headers ```http theme={null} POST /nest/transport HTTP/1.1 Host: your-server:8000 Content-Type: application/json Authorization: Basic X-nl-protocol-version: 1 ``` ### Body The device sends an `objects` array with its current bucket revisions: ```json theme={null} { "chunked": true, "session": "18b430SERIAL", "objects": [ { "object_key": "device.09AA01AB12345678", "object_revision": 42, "object_timestamp": 1707148800000 }, { "object_key": "shared.09AA01AB12345678", "object_revision": 15, "object_timestamp": 1707148800000 }, { "object_key": "schedule.09AA01AB12345678", "object_revision": 3, "object_timestamp": 1707148800000 } ] } ``` The device may also send **inline updates** by including a `value` field with `object_revision: 0` and `object_timestamp: 0`: ```json theme={null} { "chunked": true, "session": "18b430SERIAL", "objects": [ { "object_key": "shared.09AA01AB12345678", "object_revision": 0, "object_timestamp": 0, "value": { "target_temperature": 22.0, "target_temperature_type": "heat" } } ] } ``` ### Request Body Fields | Field | Type | Description | | ---------------------------- | ------- | -------------------------------------------------------------------- | | `chunked` | boolean | Always `true` — device expects chunked response | | `session` | string | Persistent session identifier (reused across reconnects) | | `objects` | array | Bucket descriptors with device's current state | | `objects[].object_key` | string | Bucket key, e.g., `device.09AA01AB12345678` | | `objects[].object_revision` | integer | Device's current revision for this bucket | | `objects[].object_timestamp` | integer | Device's current timestamp for this bucket (ms) | | `objects[].value` | object | (Inline update only) New field values; only used when rev=0 and ts=0 | ## Response Headers ```http theme={null} HTTP/1.1 200 OK Transfer-Encoding: chunked X-nl-suspend-time-max: 300 X-nl-service-timestamp: 1707148800000 X-nl-defer-device-window: 15 ``` | Header | Description | | ---------------------------- | --------------------------------------------------------------------- | | `Transfer-Encoding: chunked` | Required — enables server push and WoWLAN sleep | | `X-nl-suspend-time-max` | Device's safety-net wake timer in seconds (default: 300) | | `X-nl-service-timestamp` | Server timestamp in ms — device uses for sync decisions | | `X-nl-defer-device-window` | Seconds to delay device PUT after local changes (batches dial jitter) | | `X-nl-disable-defer-window` | Seconds to temporarily disable defer delay after a server push | ## Response Body (Chunked) When the server has data to push, it sends a JSON object in a chunked body: ``` {hex_size}\r\n {"objects": [...]}\r\n 0\r\n \r\n ``` The JSON payload uses an `objects` array: ```json theme={null} { "objects": [ { "object_revision": 16, "object_timestamp": 1707149000000, "object_key": "shared.09AA01AB12345678", "value": { "target_temperature": 22.0, "target_temperature_type": "heat" } } ] } ``` **Field ordering is critical.** `object_revision` and `object_timestamp` **must appear before** `object_key` in each object. Incorrect field ordering can cause the device to silently ignore the update or fail to parse it. **Correct:** `{"object_revision": 16, "object_timestamp": ..., "object_key": "...", "value": {...}}` **Wrong:** `{"object_key": "...", "object_revision": 16, ...}` ### When No Data Is Available If the server has no updates to send immediately, it **holds the connection open silently** (no body, no empty JSON). It does NOT send an empty `{}` or an empty `objects` array. The connection is held until: * Data arrives to push (then a chunk is sent) * `connection_hold_timeout` seconds elapse (default: `suspend_time_max - 10 = 290s`), at which point the connection closes with no body ## Synchronization Logic The server compares timestamps (not revisions) to decide what to push: * If `server_timestamp > device_timestamp` for a bucket → include it in the response * If `device_timestamp > server_timestamp` → merge the inline value into server state (if it's an inline update) * **Timestamp = 0** is a sentinel meaning "no data for this bucket yet" ## Security Considerations The server accepts all credentials. The device serial is extracted from the Basic Auth user ID (`d.{SERIAL}.{suffix}`) and used for state lookup. ## Related Device-to-server state updates Deep dive on the subscribe/push mechanism # POST /nest/upload Source: https://docs.nolongerevil.com/api-reference/thermostat/upload Device log upload endpoint ## Overview Receives device log files uploaded by the thermostat. The device periodically uploads diagnostic logs to this endpoint. This endpoint is part of the **Device Protocol API** on port 8000. It is called directly by thermostat firmware. ## Endpoint ``` POST http://your-server:8000/nest/upload ``` ## Request The device sends raw bytes (often gzip-compressed) as the request body. The `Content-Type` may vary by firmware version. ```http theme={null} POST /nest/upload HTTP/1.1 Host: your-server:8000 Authorization: Basic Content-Type: application/octet-stream ``` ## Response ### Success (200 OK) ```json theme={null} {"status": "ok"} ``` The server always returns `200 OK` after reading the body. The response is the same regardless of whether log storage is enabled. ## Log Storage By default, uploaded logs are discarded. To store them on disk, set `STORE_DEVICE_LOGS=true` in the server environment. When enabled: * Logs are decompressed (if gzip) and saved to `/app/data/device_logs/{serial}/{timestamp}.log` * Each upload creates a new file with the UTC timestamp as the filename This is useful for debugging firmware issues or analyzing device behavior. # GET /nest/weather/v1 Source: https://docs.nolongerevil.com/api-reference/thermostat/weather Weather proxy endpoint for thermostat weather data ## Overview Proxies weather data requests to `weather.nest.com` with server-side caching. Thermostats call this endpoint to get local weather conditions for display and eco mode decisions. This endpoint is part of the **Device Protocol API** on port 8000. It is called by thermostat firmware — not by you. ## Endpoint ``` GET http://your-server:8000/nest/weather/v1 GET http://your-server:8000/nest/weather/{path} ``` The second form handles any path under `/nest/weather/` for compatibility with firmware variants. ## Query Parameters The endpoint passes all query parameters through to the upstream Nest weather API. | Parameter | Description | | ------------- | -------------------------- | | `postal_code` | Postal or ZIP code | | `country` | Country code (e.g., `US`) | | Others | Passed through to Nest API | ## Response Returns the upstream weather JSON from `weather.nest.com`, cached on the server for `WEATHER_CACHE_TTL_MS` milliseconds (default: 600,000 ms = 10 minutes). ### Success (200 OK) Returns weather data as proxied from Nest's weather API (format defined by Nest). ### Error (502 Bad Gateway) ```json theme={null} {"error": "Weather service unavailable"} ``` Returned if the upstream weather API is unreachable or the request fails. ## Caching Weather responses are cached to avoid hammering the upstream API when multiple devices request weather simultaneously. The cache TTL is configurable via the `WEATHER_CACHE_TTL_MS` environment variable. ## Note on Weather Data The thermostat uses weather data for: * Outdoor temperature display * Eco mode boundary decisions * Sunrise/sunset calculations for display brightness If your deployment has no internet access, weather data will be unavailable and these features will be non-functional. # Delete Device Source: https://docs.nolongerevil.com/api-reference/v1/delete-device Remove a device from your account ## Overview Permanently remove a device from your account. This action cannot be undone. **Hosted service only.** This endpoint is part of the hosted API at `https://nolongerevil.com/api/v1` and requires an `nle_` API key. It is **not available** on the self-hosted server. **This action is irreversible!** The device will be unlinked from your account and will need to be reclaimed using an entry key. ## Endpoint ``` DELETE https://nolongerevil.com/api/v1/thermostat/{deviceId} ``` ## Authentication **Required Scopes**: `write` **Additional Requirement**: Must be the device owner (not just shared access) ## Request ### Path Parameters | Parameter | Type | Required | Description | | ---------- | ------ | -------- | ------------------- | | `deviceId` | string | Yes | Device ID to delete | ### Headers | Header | Value | Required | | --------------- | ------------------------------ | -------- | | `Authorization` | `Bearer nle_your_api_key_here` | Yes | ### Example Request ``` DELETE /api/v1/thermostat/dev_abc123xyz Authorization: Bearer nle_your_api_key_here ``` ## Response ### Success Response (200 OK) ```json theme={null} { "success": true, "message": "Device deleted successfully" } ``` ### Error Responses #### 403 Forbidden ```json theme={null} { "error": "Only device owners can delete devices" } ``` **Cause**: You have shared access to this device but are not the owner. **Solution**: Only the device owner can delete it. If you want to remove your access, ask the owner to revoke the share. #### 404 Not Found ```json theme={null} { "error": "Device not found" } ``` **Cause**: Device ID doesn't exist or you don't have access to it. ## Code Examples ```bash cURL theme={null} curl -X DELETE https://nolongerevil.com/api/v1/thermostat/dev_abc123xyz \ -H "Authorization: Bearer nle_your_api_key_here" ``` ```javascript JavaScript theme={null} async function deleteDevice(deviceId) { const response = await fetch( `https://nolongerevil.com/api/v1/thermostat/${deviceId}`, { method: 'DELETE', headers: { 'Authorization': 'Bearer nle_your_api_key_here' } } ); if (!response.ok) { throw new Error(`Failed to delete device: ${response.status}`); } return await response.json(); } // Usage with confirmation if (confirm('Are you sure you want to delete this device?')) { await deleteDevice('dev_abc123xyz'); console.log('Device deleted successfully'); } ``` ```python Python theme={null} def delete_device(device_id): response = requests.delete( f'https://nolongerevil.com/api/v1/thermostat/{device_id}', headers={'Authorization': 'Bearer nle_your_api_key_here'} ) response.raise_for_status() return response.json() # Usage with confirmation if input('Delete device? (yes/no): ').lower() == 'yes': delete_device('dev_abc123xyz') print('Device deleted') ``` ## What Happens After Deletion When you delete a device: 1. **Device is unlinked** from your account 2. **All shares are revoked** - users with shared access lose access 3. **Device settings persist** - temperature, schedule, etc. remain on the device 4. **Device remains functional** - continues operating with last known settings 5. **Can be reclaimed** - Generate a new entry key on the thermostat to reclaim it ## Use Cases ### Device Replacement ```javascript theme={null} // When replacing an old thermostat async function replaceDevice(oldDeviceId, newEntryKey) { // Delete old device await deleteDevice(oldDeviceId); // Claim new device await claimDevice(newEntryKey); } ``` ### Selling/Moving ```javascript theme={null} // When selling your home or moving out async function removeAllDevices() { const { devices } = await listDevices(); for (const device of devices) { if (device.accessType === 'owner') { await deleteDevice(device.id); console.log(`Deleted ${device.name || device.serial}`); } } } ``` ## Reclaiming a Deleted Device To reclaim a device after deletion: 1. On the thermostat: Settings → Nest App → Get Entry Code 2. In dashboard: Add Device → Enter the entry code 3. Device will be linked to your account again Before deleting a device, consider if you just want to transfer ownership instead. Contact support for ownership transfer assistance. ## Next Steps View all your devices Check device status Manage API keys # List Devices Source: https://docs.nolongerevil.com/api-reference/v1/devices Get all thermostats accessible by your API key ## Overview The `/devices` endpoint returns a list of all thermostats you have access to, including devices you own and devices that have been shared with you. ## Endpoint ``` GET https://nolongerevil.com/api/v1/devices ``` **Hosted service only.** This endpoint is part of the hosted API at `https://nolongerevil.com/api/v1` and requires an `nle_` API key. It is **not available** on the self-hosted server. ## Authentication This endpoint requires API key authentication. **Required Scopes**: `read` Include your API key in the `Authorization` header: ``` Authorization: Bearer nle_your_api_key_here ``` ## Request ### Headers | Header | Value | Required | | --------------- | ------------------------------ | -------- | | `Authorization` | `Bearer nle_your_api_key_here` | Yes | ### Query Parameters None. ## Response ### Success Response (200 OK) ```json theme={null} { "devices": [ { "id": "dev_abc123xyz", "serial": "02AA01AB01234567", "name": "Living Room", "accessType": "owner" }, { "id": "dev_def456uvw", "serial": "02AA01AB01234568", "name": "Bedroom", "accessType": "shared" } ] } ``` ### Response Fields | Field | Type | Description | | ---------------------- | -------------- | ------------------------------------- | | `devices` | array | Array of device objects | | `devices[].id` | string | Unique device identifier (UUID) | | `devices[].serial` | string | Device serial number (17 characters) | | `devices[].name` | string \| null | Custom device name (null if not set) | | `devices[].accessType` | string | Access level: `"owner"` or `"shared"` | ### Error Responses #### 401 Unauthorized ```json theme={null} { "error": "Unauthorized" } ``` **Cause**: Missing or invalid API key. **Solution**: Check that your API key is correct and included in the Authorization header. #### 429 Too Many Requests ```json theme={null} { "error": "Rate limit exceeded", "retryAfter": "2025-01-24T12:35:00.000Z" } ``` **Cause**: You've exceeded the rate limit (20 requests/minute for API keys). **Solution**: Wait until the time specified in `retryAfter` before making another request. ## Rate Limiting | Rate Limit | Value | | ------------------- | ----------------------------------- | | Requests per minute | 20 (API keys) / 100 (user accounts) | **Rate Limit Headers** (included in response): ``` X-RateLimit-Limit: 20 X-RateLimit-Remaining: 15 X-RateLimit-Reset: 2025-01-24T12:34:56.000Z ``` ## Code Examples ```bash cURL theme={null} curl https://nolongerevil.com/api/v1/devices \ -H "Authorization: Bearer nle_your_api_key_here" ``` ```javascript JavaScript/Node.js theme={null} const API_KEY = 'nle_your_api_key_here'; const BASE_URL = 'https://nolongerevil.com/api/v1'; async function listDevices() { const response = await fetch(`${BASE_URL}/devices`, { headers: { 'Authorization': `Bearer ${API_KEY}` } }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); return data.devices; } // Usage listDevices() .then(devices => { console.log('Found devices:', devices); devices.forEach(device => { console.log(`- ${device.name || device.serial} (${device.accessType})`); }); }) .catch(error => console.error('Error:', error)); ``` ```python Python theme={null} import requests API_KEY = 'nle_your_api_key_here' BASE_URL = 'https://nolongerevil.com/api/v1' def list_devices(): headers = { 'Authorization': f'Bearer {API_KEY}' } response = requests.get(f'{BASE_URL}/devices', headers=headers) response.raise_for_status() return response.json()['devices'] # Usage try: devices = list_devices() print(f'Found {len(devices)} device(s):') for device in devices: name = device['name'] or device['serial'] print(f'- {name} ({device["accessType"]})') except requests.exceptions.HTTPError as e: print(f'Error: {e}') ``` ## Use Cases ### Home Automation Integration List all your devices at startup to build a device registry: ```javascript theme={null} // Home Assistant integration example async function discoverDevices() { const devices = await listDevices(); // Register each device with your automation platform devices.forEach(device => { registerEntity({ id: device.id, name: device.name || `Thermostat ${device.serial}`, type: 'climate', capabilities: ['temperature', 'mode', 'fan'] }); }); } ``` ### Multi-Zone Control Control devices in different zones: ```javascript theme={null} const devices = await listDevices(); // Group by location/name const upstairs = devices.filter(d => d.name && d.name.toLowerCase().includes('upstairs') ); const downstairs = devices.filter(d => d.name && d.name.toLowerCase().includes('downstairs') ); // Set different temperatures await setZoneTemperature(upstairs, 22); // 22°C upstairs await setZoneTemperature(downstairs, 20); // 20°C downstairs ``` ### Monitoring Dashboard Build a dashboard showing all your thermostats: ```javascript theme={null} async function buildDashboard() { const devices = await listDevices(); // Fetch status for each device const deviceStatuses = await Promise.all( devices.map(async (device) => { const status = await getDeviceStatus(device.id); return { ...device, temperature: status.state['shared.' + device.serial]?.value.current_temperature, targetTemp: status.state['shared.' + device.serial]?.value.target_temperature }; }) ); return deviceStatuses; } ``` ## Access Control The devices returned depend on your API key's permissions: 1. **No device restrictions**: Returns all devices you own or have shared access to 2. **Specific device restrictions**: Returns only devices specified in the API key's `serials` array Devices shared with you will have `accessType: "shared"`. You may have limited permissions on shared devices depending on the sharing settings. ## Next Steps Retrieve detailed device state and settings Control device temperature Change HVAC mode (heat/cool/auto/off) Learn about API key management # Get Device Status Source: https://docs.nolongerevil.com/api-reference/v1/get-status Retrieve complete device state including temperature, mode, and settings ## Overview The `/thermostat/{deviceId}/status` endpoint returns the complete state of a thermostat, including current temperature, target temperature, HVAC mode, fan settings, schedule, and all other device properties. ## Endpoint ``` GET https://nolongerevil.com/api/v1/thermostat/{deviceId}/status ``` **Hosted service only.** This endpoint is part of the hosted API at `https://nolongerevil.com/api/v1` and requires an `nle_` API key. It is **not available** on the self-hosted server. ## Authentication **Required Scopes**: `read` Include your API key in the `Authorization` header: ``` Authorization: Bearer nle_your_api_key_here ``` ## Request ### Path Parameters | Parameter | Type | Required | Description | | ---------- | ------ | -------- | --------------------------------------------- | | `deviceId` | string | Yes | Device ID (UUID) from the `/devices` endpoint | ### Headers | Header | Value | Required | | --------------- | ------------------------------ | -------- | | `Authorization` | `Bearer nle_your_api_key_here` | Yes | ### Example Request ```bash theme={null} GET https://nolongerevil.com/api/v1/thermostat/dev_abc123xyz/status Authorization: Bearer nle_your_api_key_here ``` ## Response ### Success Response (200 OK) ```json theme={null} { "device": { "id": "dev_abc123xyz", "serial": "02AA01AB01234567", "name": "Living Room" }, "state": { "shared.02AA01AB01234567": { "object_revision": 152, "object_timestamp": 1764026373459, "value": { "target_temperature": 21.5, "target_temperature_type": "heat", "current_temperature": 20.2, "hvac_heater_state": true, "hvac_ac_state": false, "fan_mode": "auto", "auto_away": 0, "leaf": false, "can_cool": true, "can_heat": true, "postal_code": "85388", "country_code": "US" } }, "device.02AA01AB01234567": { "object_revision": 89, "object_timestamp": 1764026373000, "value": { "temperature_scale": "C", "fan_timer_timeout": 0, "compressor_lockout_enabled": true, "eco_mode_enabled": false, "temperature_lock_enabled": false } }, "user.67032595": { "object_revision": -23671, "object_timestamp": 1764024643855, "value": { "away": false, "name": "Guest", "email": "guest@nolongerevil.com" } }, "structure.95d0a01f-1c25-447f-ba61-edb275e9ae25": { "object_revision": -15172, "object_timestamp": 1764024645708, "value": { "name": "Home", "away": false, "postal_code": "85388", "country_code": "US", "time_zone": "America/Chicago" } } } } ``` ### Response Fields | Field | Type | Description | | ------------------------------- | -------------- | --------------------------------------------- | | `device` | object | Basic device information | | `device.id` | string | Device UUID | | `device.serial` | string | Device serial number | | `device.name` | string \| null | Custom device name | | `state` | object | Object containing all device state data | | `state.shared.{serial}` | object | Shared device state (temperature, mode, etc.) | | `state.device.{serial}` | object | Device-specific settings | | `state.user.{userId}` | object | User-specific settings | | `state.structure.{structureId}` | object | Structure/home settings | ### Key State Fields (shared..value) | Field | Type | Description | | ------------------------- | ------- | --------------------------------- | | `current_temperature` | number | Current room temperature (°C) | | `target_temperature` | number | Target temperature (°C) | | `target_temperature_type` | string | `"heat"`, `"cool"`, or `"range"` | | `target_temperature_low` | number | Low temp for heat-cool mode (°C) | | `target_temperature_high` | number | High temp for heat-cool mode (°C) | | `hvac_heater_state` | boolean | Heating currently active | | `hvac_ac_state` | boolean | Cooling currently active | | `hvac_fan_state` | boolean | Fan currently running | | `fan_mode` | string | `"auto"`, `"on"`, or `"off"` | | `auto_away` | number | 0 = home, 2 = away | | `can_cool` | boolean | Device supports cooling | | `can_heat` | boolean | Device supports heating | ### Error Responses #### 401 Unauthorized ```json theme={null} { "error": "Unauthorized" } ``` **Cause**: Missing or invalid API key. #### 403 Forbidden ```json theme={null} { "error": "Access denied to this device" } ``` **Cause**: Your API key doesn't have permission to access this device. **Solution**: Check that the device ID is correct and your API key has access to it. #### 404 Not Found ```json theme={null} { "error": "Device not found" } ``` **Cause**: Device ID doesn't exist or you don't have access to it. #### 429 Too Many Requests ```json theme={null} { "error": "Rate limit exceeded", "retryAfter": "2025-01-24T12:35:00.000Z" } ``` ## Code Examples ```bash cURL theme={null} curl https://nolongerevil.com/api/v1/thermostat/dev_abc123xyz/status \ -H "Authorization: Bearer nle_your_api_key_here" ``` ```javascript JavaScript/Node.js theme={null} const API_KEY = 'nle_your_api_key_here'; const BASE_URL = 'https://nolongerevil.com/api/v1'; async function getDeviceStatus(deviceId) { const response = await fetch( `${BASE_URL}/thermostat/${deviceId}/status`, { headers: { 'Authorization': `Bearer ${API_KEY}` } } ); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return await response.json(); } // Usage getDeviceStatus('dev_abc123xyz') .then(data => { const shared = data.state[`shared.${data.device.serial}`]?.value; console.log(`Current temp: ${shared.current_temperature}°C`); console.log(`Target temp: ${shared.target_temperature}°C`); console.log(`Mode: ${shared.target_temperature_type}`); console.log(`Heating: ${shared.hvac_heater_state ? 'ON' : 'OFF'}`); }) .catch(error => console.error('Error:', error)); ``` ```python Python theme={null} import requests API_KEY = 'nle_your_api_key_here' BASE_URL = 'https://nolongerevil.com/api/v1' def get_device_status(device_id): headers = { 'Authorization': f'Bearer {API_KEY}' } response = requests.get( f'{BASE_URL}/thermostat/{device_id}/status', headers=headers ) response.raise_for_status() return response.json() # Usage try: data = get_device_status('dev_abc123xyz') serial = data['device']['serial'] shared = data['state'][f'shared.{serial}']['value'] print(f"Current temp: {shared['current_temperature']}°C") print(f"Target temp: {shared['target_temperature']}°C") print(f"Mode: {shared['target_temperature_type']}") print(f"Heating: {'ON' if shared['hvac_heater_state'] else 'OFF'}") except requests.exceptions.HTTPError as e: print(f'Error: {e}') ``` ## Use Cases ### Temperature Monitoring Poll device status to monitor temperature changes: ```javascript theme={null} async function monitorTemperature(deviceId, callback) { setInterval(async () => { const status = await getDeviceStatus(deviceId); const shared = status.state[`shared.${status.device.serial}`]?.value; callback({ current: shared.current_temperature, target: shared.target_temperature, heating: shared.hvac_heater_state, cooling: shared.hvac_ac_state }); }, 60000); // Every minute } monitorTemperature('dev_abc123xyz', (temps) => { console.log(`Room: ${temps.current}°C → ${temps.target}°C`); }); ``` ### Energy Usage Tracking Track HVAC system activity: ```javascript theme={null} async function logEnergyUsage(deviceId) { const status = await getDeviceStatus(deviceId); const shared = status.state[`shared.${status.device.serial}`]?.value; const timestamp = new Date().toISOString(); const usage = { timestamp, heating: shared.hvac_heater_state, cooling: shared.hvac_ac_state, fan: shared.hvac_fan_state, currentTemp: shared.current_temperature, targetTemp: shared.target_temperature }; // Save to database or analytics platform await saveToDatabase(usage); } ``` ### Smart Automations Build conditional logic based on device state: ```javascript theme={null} async function smartAwayMode(deviceId) { const status = await getDeviceStatus(deviceId); const shared = status.state[`shared.${status.device.serial}`]?.value; // If home and temperature is comfortable, don't run HVAC if (shared.auto_away === 0) { // Home const tempDiff = Math.abs( shared.current_temperature - shared.target_temperature ); if (tempDiff < 0.5 && !shared.hvac_heater_state && !shared.hvac_ac_state) { console.log('Temperature comfortable, HVAC idle'); // Could adjust target temp to save energy } } } ``` ## State Object Keys The `state` object contains multiple sub-objects with different types of data: * **`shared.{serial}`**: Shared device state (temperature, mode, HVAC status) * **`device.{serial}`**: Device-specific settings (scale, timers, locks) * **`user.{userId}`**: User preferences and settings * **`structure.{structureId}`**: Home/structure-wide settings * **`schedule.{serial}`**: Device schedule (if configured) Object revisions (`object_revision`) and timestamps (`object_timestamp`) are used for state synchronization and conflict resolution. You typically don't need to use these values. ## Next Steps Change the target temperature Change HVAC mode Toggle between home and away Get all your devices # Get/Update Schedule Source: https://docs.nolongerevil.com/api-reference/v1/schedule Retrieve or modify the thermostat schedule ## Overview Get the current thermostat schedule or update it with a new schedule configuration. **Hosted service only.** This endpoint is part of the hosted API at `https://nolongerevil.com/api/v1` and requires an `nle_` API key. It is **not available** on the self-hosted server. ## Endpoints ``` GET https://nolongerevil.com/api/v1/thermostat/{deviceId}/schedule PUT https://nolongerevil.com/api/v1/thermostat/{deviceId}/schedule ``` ## Authentication **Required Scopes**: * `read` for GET * `write` for PUT ## Get Schedule ### Request ``` GET /api/v1/thermostat/{deviceId}/schedule Authorization: Bearer nle_your_api_key_here ``` ### Response (200 OK) ```json theme={null} { "device": { "id": "dev_abc123xyz", "serial": "02AA01AB01234567", "name": "Living Room" }, "schedule": { "ver": 2, "days": { "0": { "0": { "temp": 24.83397, "time": 0, "type": "COOL", "entry_type": "continuation" }, "1": { "temp": 22.0, "time": 61200, "type": "COOL", "entry_type": "setpoint" } }, "1": { ... }, ... }, "name": "Current Schedule", "schedule_mode": "COOL" } } ``` ### Response if No Schedule ```json theme={null} { "device": { ... }, "schedule": null } ``` ## Update Schedule ### Request ``` PUT /api/v1/thermostat/{deviceId}/schedule Authorization: Bearer nle_your_api_key_here Content-Type: application/json ``` ### Request Body | Field | Type | Required | Description | | ---------- | ------ | -------- | ------------------------ | | `schedule` | object | Yes | Complete schedule object | ```json theme={null} { "schedule": { "ver": 2, "days": { ... }, "name": "Weekly Schedule", "schedule_mode": "HEAT" } } ``` ## Schedule Structure ### Days Object Keys are day of week (0 = Sunday, 6 = Saturday): ```json theme={null} { "0": { ... }, // Sunday "1": { ... }, // Monday "2": { ... }, // Tuesday ... "6": { ... } // Saturday } ``` ### Schedule Entry Each day contains numbered entries (0, 1, 2, ...): | Field | Type | Description | | ------------ | ------ | -------------------------------- | | `temp` | number | Temperature in Celsius | | `time` | number | Seconds since midnight (0-86400) | | `type` | string | `"HEAT"`, `"COOL"`, or `"RANGE"` | | `entry_type` | string | `"setpoint"` or `"continuation"` | **Example**: 6:00 AM = `21600` seconds (6 × 60 × 60) ## Code Examples ```bash cURL - Get theme={null} curl https://nolongerevil.com/api/v1/thermostat/dev_abc123xyz/schedule \ -H "Authorization: Bearer nle_your_api_key_here" ``` ```javascript JavaScript - Get theme={null} async function getSchedule(deviceId) { const response = await fetch( `https://nolongerevil.com/api/v1/thermostat/${deviceId}/schedule`, { headers: { 'Authorization': 'Bearer nle_your_api_key_here' } } ); return await response.json(); } // Usage const { schedule } = await getSchedule('dev_abc123xyz'); ``` ```javascript JavaScript - Update theme={null} async function updateSchedule(deviceId, scheduleData) { const response = await fetch( `https://nolongerevil.com/api/v1/thermostat/${deviceId}/schedule`, { method: 'PUT', headers: { 'Authorization': 'Bearer nle_your_api_key_here', 'Content-Type': 'application/json' }, body: JSON.stringify({ schedule: scheduleData }) } ); return await response.json(); } ``` ## Use Cases ### Simple Weekly Schedule ```javascript theme={null} const weekdaySchedule = { ver: 2, name: "Weekday Schedule", schedule_mode: "HEAT", days: { 1: { // Monday 0: { temp: 18, time: 0, type: "HEAT", entry_type: "continuation" }, 1: { temp: 21, time: 21600, type: "HEAT", entry_type: "setpoint" }, // 6 AM 2: { temp: 18, time: 32400, type: "HEAT", entry_type: "setpoint" }, // 9 AM 3: { temp: 22, time: 61200, type: "HEAT", entry_type: "setpoint" } // 5 PM }, // Repeat for Tuesday-Friday (2-5) ... } }; await updateSchedule(deviceId, weekdaySchedule); ``` ### Copy Schedule ```javascript theme={null} // Copy schedule from one device to another const { schedule } = await getSchedule(sourceDeviceId); await updateSchedule(targetDeviceId, schedule); ``` Schedules are complex objects. It's recommended to get the existing schedule, modify it, and update it rather than creating from scratch. ## Next Steps Manual temperature control Check current settings # Set Away Mode Source: https://docs.nolongerevil.com/api-reference/v1/set-away Toggle between home and away modes for energy savings ## Overview Enable or disable away mode. When away mode is enabled, the thermostat adjusts to energy-saving temperatures. **Hosted service only.** This endpoint is part of the hosted API at `https://nolongerevil.com/api/v1` and requires an `nle_` API key. It is **not available** on the self-hosted server. ## Endpoint ``` POST https://nolongerevil.com/api/v1/thermostat/{deviceId}/away ``` ## Authentication **Required Scopes**: `write` ## Request Body | Field | Type | Required | Description | | ------ | ------- | -------- | --------------------------------- | | `away` | boolean | Yes | `true` for away, `false` for home | ```json theme={null} { "away": true } ``` ## Response Success (200 OK): ```json theme={null} { "success": true, "message": "Command handled", "device": "02AA01AB01234567", "object": "shared.02AA01AB01234567", "revision": 156, "timestamp": 1764026405000 } ``` ## Away Mode Effects When away mode is enabled: * **Temperature adjusted**: Lower in winter, higher in summer * **Motion detection paused**: Won't automatically switch to home * **Energy savings**: Reduced HVAC usage * **Learning disabled**: Thermostat won't learn new patterns Away mode can save 10-20% on energy costs when you're not home. ## Code Examples ```bash cURL theme={null} # Enable away mode curl -X POST https://nolongerevil.com/api/v1/thermostat/dev_abc123xyz/away \ -H "Authorization: Bearer nle_your_api_key_here" \ -H "Content-Type: application/json" \ -d '{"away": true}' ``` ```javascript JavaScript theme={null} async function setAwayMode(deviceId, away) { const response = await fetch( `https://nolongerevil.com/api/v1/thermostat/${deviceId}/away`, { method: 'POST', headers: { 'Authorization': 'Bearer nle_your_api_key_here', 'Content-Type': 'application/json' }, body: JSON.stringify({ away }) } ); return await response.json(); } // Usage await setAwayMode('dev_abc123xyz', true); // Enable away await setAwayMode('dev_abc123xyz', false); // Disable away ``` ```python Python theme={null} def set_away_mode(device_id, away): response = requests.post( f'https://nolongerevil.com/api/v1/thermostat/{device_id}/away', headers={'Authorization': 'Bearer nle_your_api_key_here'}, json={'away': away} ) response.raise_for_status() return response.json() # Enable away mode set_away_mode('dev_abc123xyz', True) ``` ## Use Cases ### Geofencing Integration ```javascript theme={null} // Automatically enable away mode when you leave home async function handleGeofence(location) { const isHome = calculateDistance(location, homeLocation) < 0.5; // Within 0.5 miles await setAwayMode(deviceId, !isHome); } ``` ### Vacation Mode ```javascript theme={null} // Enable away mode for vacation async function enableVacationMode(deviceId, startDate, endDate) { await setAwayMode(deviceId, true); // Schedule return to home mode scheduleTask(endDate, async () => { await setAwayMode(deviceId, false); }); } ``` ### Energy-Aware Automation ```javascript theme={null} // Enable away during peak energy hours if (hour >= 14 && hour <= 18) { // 2PM-6PM peak hours await setAwayMode(deviceId, true); } ``` ## Next Steps Adjust target temperature Check away status # Control Fan Source: https://docs.nolongerevil.com/api-reference/v1/set-fan Set fan mode or start a fan timer ## Overview Control the thermostat's fan independently of heating/cooling. Set the fan to always on, auto (runs with HVAC), or start a timed fan run. **Hosted service only.** This endpoint is part of the hosted API at `https://nolongerevil.com/api/v1` and requires an `nle_` API key. It is **not available** on the self-hosted server. ## Endpoint ``` POST https://nolongerevil.com/api/v1/thermostat/{deviceId}/fan ``` ## Authentication **Required Scopes**: `write` ## Request Body **For fan mode**: | Field | Type | Required | Description | | ------ | ------ | -------- | ---------------------------- | | `mode` | string | Yes | `"on"`, `"auto"`, or `"off"` | **For fan timer**: | Field | Type | Required | Description | | ---------- | ------ | -------- | ------------------------------------------ | | `duration` | number | Yes | Duration in seconds (max 43200 = 12 hours) | ### Example: Set Fan Mode ```json theme={null} { "mode": "on" } ``` ### Example: Start Fan Timer ```json theme={null} { "duration": 900 } ``` ## Response Success (200 OK): ```json theme={null} { "success": true, "message": "Command handled", "device": "02AA01AB01234567", "object": "device.02AA01AB01234567", "revision": 90, "timestamp": 1764026410000 } ``` ## Fan Modes | Mode | Description | | ------ | -------------------------------------------- | | `on` | Fan runs continuously | | `auto` | Fan runs only when heating/cooling is active | | `off` | Fan off (system idle) | Fan timers automatically set the fan to `auto` mode after the duration expires. ## Code Examples ```bash cURL - Set Mode theme={null} curl -X POST https://nolongerevil.com/api/v1/thermostat/dev_abc123xyz/fan \ -H "Authorization: Bearer nle_your_api_key_here" \ -H "Content-Type: application/json" \ -d '{"mode": "on"}' ``` ```bash cURL - Start Timer theme={null} curl -X POST https://nolongerevil.com/api/v1/thermostat/dev_abc123xyz/fan \ -H "Authorization: Bearer nle_your_api_key_here" \ -H "Content-Type: application/json" \ -d '{"duration": 900}' ``` ```javascript JavaScript theme={null} async function setFanMode(deviceId, mode) { const response = await fetch( `https://nolongerevil.com/api/v1/thermostat/${deviceId}/fan`, { method: 'POST', headers: { 'Authorization': 'Bearer nle_your_api_key_here', 'Content-Type': 'application/json' }, body: JSON.stringify({ mode }) } ); return await response.json(); } async function startFanTimer(deviceId, durationSeconds) { const response = await fetch( `https://nolongerevil.com/api/v1/thermostat/${deviceId}/fan`, { method: 'POST', headers: { 'Authorization': 'Bearer nle_your_api_key_here', 'Content-Type': 'application/json' }, body: JSON.stringify({ duration: durationSeconds }) } ); return await response.json(); } // Usage await setFanMode('dev_abc123xyz', 'on'); // Fan always on await startFanTimer('dev_abc123xyz', 900); // Run fan for 15 minutes ``` ```python Python theme={null} def set_fan_mode(device_id, mode): response = requests.post( f'https://nolongerevil.com/api/v1/thermostat/{device_id}/fan', headers={'Authorization': 'Bearer nle_your_api_key_here'}, json={'mode': mode} ) response.raise_for_status() return response.json() def start_fan_timer(device_id, duration_seconds): response = requests.post( f'https://nolongerevil.com/api/v1/thermostat/{device_id}/fan', headers={'Authorization': 'Bearer nle_your_api_key_here'}, json={'duration': duration_seconds} ) response.raise_for_status() return response.json() ``` ## Use Cases ### Air Circulation ```javascript theme={null} // Run fan for 15 minutes every hour to circulate air setInterval(async () => { await startFanTimer(deviceId, 15 * 60); // 15 minutes }, 60 * 60 * 1000); // Every hour ``` ### Post-Cooking Ventilation ```javascript theme={null} // Run fan after cooking to remove odors await startFanTimer(deviceId, 30 * 60); // 30 minutes ``` ## Next Steps Check fan status Control temperature # Temperature Lock Source: https://docs.nolongerevil.com/api-reference/v1/set-lock Enable or disable temperature lock with PIN protection ## Overview Enable temperature lock to restrict temperature adjustments on the physical thermostat. Requires a 4-digit PIN. **Hosted service only.** This endpoint is part of the hosted API at `https://nolongerevil.com/api/v1` and requires an `nle_` API key. It is **not available** on the self-hosted server. ## Endpoint ``` POST https://nolongerevil.com/api/v1/thermostat/{deviceId}/lock ``` ## Authentication **Required Scopes**: `write` ## Request Body **To enable lock**: | Field | Type | Required | Description | | ---------- | ------- | -------- | -------------------------------- | | `enabled` | boolean | Yes | `true` | | `pin_hash` | string | Yes | Base64(SHA1(PIN + SERIAL)) | | `temp_min` | number | Yes | Minimum allowed temperature (°C) | | `temp_max` | number | Yes | Maximum allowed temperature (°C) | **To disable lock**: | Field | Type | Required | Description | | --------- | ------- | -------- | ----------- | | `enabled` | boolean | Yes | `false` | ### Example: Enable Lock ```json theme={null} { "enabled": true, "pin_hash": "/PiS0Kl52c8oNptmsL4Z7cxd9mg=", "temp_min": 18, "temp_max": 24 } ``` The `pin_hash` is Base64(SHA1(PIN + SERIAL)). **Never send the PIN in plaintext!** You must concatenate the PIN with the device serial number before hashing. ### Example: Disable Lock ```json theme={null} { "enabled": false } ``` ## Response Success (200 OK): ```json theme={null} { "success": true, "message": "Command handled", "device": "02AA01AB01234567", "object": "device.02AA01AB01234567", "revision": 91, "timestamp": 1764026415000 } ``` ## Generating PIN Hash The PIN hash algorithm matches the Nest thermostat's native format: **Algorithm:** `Base64( SHA1( PIN + SERIAL ) )` You must concatenate the PIN with the device's serial number before hashing. This matches the Nest protocol and makes PINs device-specific. ```javascript JavaScript (Browser) theme={null} async function hashPin(pin, serial) { // Combine PIN and serial number const combined = pin + serial; // Convert to bytes const encoder = new TextEncoder(); const data = encoder.encode(combined); // SHA-1 hash const hashBuffer = await crypto.subtle.digest('SHA-1', data); // Convert to Base64 const hashArray = Array.from(new Uint8Array(hashBuffer)); const base64 = btoa(String.fromCharCode(...hashArray)); return base64; } // Example: PIN "0000" + Serial "02AA01AB01234567" const pinHash = await hashPin('0000', '02AA01AB01234567'); // Result: /PiS0Kl52c8oNptmsL4Z7cxd9mg= ``` ```javascript JavaScript (Node.js) theme={null} import crypto from 'crypto'; function hashPin(pin, serial) { const combined = pin + serial; const hash = crypto.createHash('sha1').update(combined).digest('base64'); return hash; } // Example const pinHash = hashPin('0000', '02AA01AB01234567'); // Result: /PiS0Kl52c8oNptmsL4Z7cxd9mg= ``` ```python Python theme={null} import hashlib import base64 def hash_pin(pin, serial): combined = pin + serial sha1_hash = hashlib.sha1(combined.encode()).digest() base64_hash = base64.b64encode(sha1_hash).decode() return base64_hash # Example pin_hash = hash_pin('0000', '02AA01AB01234567') # Result: /PiS0Kl52c8oNptmsL4Z7cxd9mg= ``` ## Code Examples ```bash cURL - Enable theme={null} curl -X POST https://nolongerevil.com/api/v1/thermostat/dev_abc123xyz/lock \ -H "Authorization: Bearer nle_your_api_key_here" \ -H "Content-Type: application/json" \ -d '{ "enabled": true, "pin_hash": "/PiS0Kl52c8oNptmsL4Z7cxd9mg=", "temp_min": 18, "temp_max": 24 }' ``` ```bash cURL - Disable theme={null} curl -X POST https://nolongerevil.com/api/v1/thermostat/dev_abc123xyz/lock \ -H "Authorization: Bearer nle_your_api_key_here" \ -H "Content-Type: application/json" \ -d '{"enabled": false}' ``` ```javascript JavaScript theme={null} async function hashPin(pin, serial) { const combined = pin + serial; const encoder = new TextEncoder(); const data = encoder.encode(combined); const hashBuffer = await crypto.subtle.digest('SHA-1', data); const hashArray = Array.from(new Uint8Array(hashBuffer)); return btoa(String.fromCharCode(...hashArray)); } async function enableTemperatureLock(deviceId, serial, pin, tempMin, tempMax) { const pinHash = await hashPin(pin, serial); const response = await fetch( `https://nolongerevil.com/api/v1/thermostat/${deviceId}/lock`, { method: 'POST', headers: { 'Authorization': 'Bearer nle_your_api_key_here', 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: true, pin_hash: pinHash, temp_min: tempMin, temp_max: tempMax }) } ); return await response.json(); } // Usage (you need the device serial from GET /devices) await enableTemperatureLock('dev_abc123xyz', '02AA01AB01234567', '1234', 18, 24); ``` ## Use Cases ### Child Safety ```javascript theme={null} // Lock thermostat to prevent children from adjusting temperature await enableTemperatureLock(deviceId, deviceSerial, '1234', 18, 24); ``` ### Commercial Buildings ```javascript theme={null} // Restrict guest room temperature range in hotels await enableTemperatureLock(roomDeviceId, roomSerial, hotelPin, 20, 23); ``` ## Security Notes **Never store or transmit PINs in plaintext**. Always hash the PIN client-side before sending to the API. ### Important Points * **PIN Format**: Must be exactly 4 digits (0000-9999) * **Hashing**: Use SHA-1 (not SHA-256) with PIN + serial concatenated * **Encoding**: Hash must be Base64-encoded (not hex) * **Serial Required**: You must include the device serial number in the hash * **Temperature Units**: temp\_min and temp\_max must be in Celsius * **Device-Specific**: Same PIN produces different hashes for different devices ### Why SHA-1 + Serial? This matches the Nest thermostat's native PIN algorithm: * **Device-specific**: Same PIN produces different hashes for different thermostats * **Protocol compatibility**: Works with the original Nest hardware * **Simple validation**: Thermostat can verify the hash locally without server communication ## Next Steps Check lock status Set temperature # Set HVAC Mode Source: https://docs.nolongerevil.com/api-reference/v1/set-mode Change the heating/cooling mode of your thermostat ## Overview Change the HVAC operating mode between heat, cool, heat-cool (auto), or off. **Hosted service only.** This endpoint is part of the hosted API at `https://nolongerevil.com/api/v1` and requires an `nle_` API key. It is **not available** on the self-hosted server. ## Endpoint ``` POST https://nolongerevil.com/api/v1/thermostat/{deviceId}/mode ``` ## Authentication **Required Scopes**: `write` ## Request Body | Field | Type | Required | Description | | ------ | ------ | -------- | --------------------------------------------- | | `mode` | string | Yes | `"heat"`, `"cool"`, `"heat-cool"`, or `"off"` | ```json theme={null} { "mode": "heat-cool" } ``` ## Response Success (200 OK): ```json theme={null} { "success": true, "message": "Command handled", "device": "02AA01AB01234567", "object": "shared.02AA01AB01234567", "revision": 155, "timestamp": 1764026377395 } ``` ## HVAC Modes | Mode | Description | | ----------- | -------------------------------------------- | | `heat` | Heating only - maintains minimum temperature | | `cool` | Cooling only - maintains maximum temperature | | `heat-cool` | Auto mode - maintains temperature range | | `off` | System off - no heating or cooling | When switching to `heat-cool` mode, use the [Set Temperature Range](/api-reference/v1/set-temperature-range) endpoint to configure the temperature range. ## Code Examples ```bash cURL theme={null} curl -X POST https://nolongerevil.com/api/v1/thermostat/dev_abc123xyz/mode \ -H "Authorization: Bearer nle_your_api_key_here" \ -H "Content-Type: application/json" \ -d '{"mode": "heat-cool"}' ``` ```javascript JavaScript theme={null} async function setMode(deviceId, mode) { const response = await fetch( `https://nolongerevil.com/api/v1/thermostat/${deviceId}/mode`, { method: 'POST', headers: { 'Authorization': 'Bearer nle_your_api_key_here', 'Content-Type': 'application/json' }, body: JSON.stringify({ mode }) } ); return await response.json(); } // Usage await setMode('dev_abc123xyz', 'heat'); ``` ```python Python theme={null} def set_mode(device_id, mode): response = requests.post( f'https://nolongerevil.com/api/v1/thermostat/{device_id}/mode', headers={'Authorization': 'Bearer nle_your_api_key_here'}, json={'mode': mode} ) response.raise_for_status() return response.json() ``` ## Use Cases ### Seasonal Mode Switching ```javascript theme={null} // Switch to cooling for summer if (month >= 5 && month <= 9) { await setMode(deviceId, 'cool'); } else { await setMode(deviceId, 'heat'); } ``` ### Energy Saving ```javascript theme={null} // Turn off HVAC when nobody's home if (awayMode) { await setMode(deviceId, 'off'); } ``` ## Next Steps Set target temperature Configure heat-cool range # Set Temperature Source: https://docs.nolongerevil.com/api-reference/v1/set-temperature Set the target temperature for heating or cooling ## Overview Set the target temperature for your thermostat in heating or cooling mode. ## Endpoint ``` POST https://nolongerevil.com/api/v1/thermostat/{deviceId}/temperature ``` **Hosted service only.** This endpoint is part of the hosted API at `https://nolongerevil.com/api/v1` and requires an `nle_` API key. It is **not available** on the self-hosted server. ## Authentication **Required Scopes**: `write` ``` Authorization: Bearer nle_your_api_key_here ``` ## Request ### Path Parameters | Parameter | Type | Required | Description | | ---------- | ------ | -------- | ---------------------------------- | | `deviceId` | string | Yes | Device ID from `/devices` endpoint | ### Request Body | Field | Type | Required | Description | | ------- | ------ | -------- | ------------------------ | | `value` | number | Yes | Target temperature | | `mode` | string | Yes | `"heat"` or `"cool"` | | `scale` | string | No | `"C"` (default) or `"F"` | ### Example Request ```json theme={null} { "value": 72, "mode": "heat", "scale": "F" } ``` ## Response ### Success Response (200 OK) ```json theme={null} { "success": true, "message": "Command handled", "device": "02AA01AB01234567", "object": "shared.02AA01AB01234567", "revision": 153, "timestamp": 1764026373459 } ``` ### Error Responses **400 Bad Request**: ```json theme={null} { "error": "Temperature value must be a number" } ``` **401/403/429**: See [authentication docs](/api-reference/authentication). ## Code Examples ```bash cURL theme={null} curl -X POST https://nolongerevil.com/api/v1/thermostat/dev_abc123xyz/temperature \ -H "Authorization: Bearer nle_your_api_key_here" \ -H "Content-Type: application/json" \ -d '{"value": 72, "mode": "heat", "scale": "F"}' ``` ```javascript JavaScript theme={null} async function setTemperature(deviceId, temperature, mode, scale = 'C') { const response = await fetch( `https://nolongerevil.com/api/v1/thermostat/${deviceId}/temperature`, { method: 'POST', headers: { 'Authorization': 'Bearer nle_your_api_key_here', 'Content-Type': 'application/json' }, body: JSON.stringify({ value: temperature, mode, scale }) } ); return await response.json(); } // Usage await setTemperature('dev_abc123xyz', 72, 'heat', 'F'); ``` ```python Python theme={null} import requests def set_temperature(device_id, temperature, mode, scale='C'): response = requests.post( f'https://nolongerevil.com/api/v1/thermostat/{device_id}/temperature', headers={'Authorization': 'Bearer nle_your_api_key_here'}, json={'value': temperature, 'mode': mode, 'scale': scale} ) response.raise_for_status() return response.json() # Usage set_temperature('dev_abc123xyz', 22, 'heat', 'C') ``` ## Temperature Scale * **Celsius**: Use `"scale": "C"` (default) * **Fahrenheit**: Use `"scale": "F"` The thermostat internally uses Celsius. If you provide Fahrenheit, it will be converted automatically. Thermostats round to the nearest 0.5°C increment. ## Use Cases ### Schedule-Based Control ```javascript theme={null} const schedule = [ { time: '06:00', temp: 68 }, // Morning { time: '09:00', temp: 62 }, // Day (away) { time: '17:00', temp: 70 }, // Evening { time: '22:00', temp: 65 } // Night ]; schedule.forEach(({ time, temp }) => { cron.schedule(time, () => { setTemperature(deviceId, temp, 'heat', 'F'); }); }); ``` ### Smart Adjustments ```javascript theme={null} // Adjust based on outdoor temperature const outdoorTemp = await getWeatherTemperature(); if (outdoorTemp < 32) { await setTemperature(deviceId, 72, 'heat', 'F'); // Colder outside } else { await setTemperature(deviceId, 68, 'heat', 'F'); // Warmer outside } ``` ## Next Steps Change HVAC mode Set range for heat-cool mode Check current temperature Enable energy-saving mode # Set Temperature Range Source: https://docs.nolongerevil.com/api-reference/v1/set-temperature-range Set temperature range for heat-cool (auto) mode ## Overview Configure the temperature range for heat-cool (auto) mode. The thermostat will heat when below the low threshold and cool when above the high threshold. **Hosted service only.** This endpoint is part of the hosted API at `https://nolongerevil.com/api/v1` and requires an `nle_` API key. It is **not available** on the self-hosted server. ## Endpoint ``` POST https://nolongerevil.com/api/v1/thermostat/{deviceId}/temperature/range ``` ## Authentication **Required Scopes**: `write` ## Request Body | Field | Type | Required | Description | | ------- | ------ | -------- | --------------------------------------- | | `low` | number | Yes | Minimum temperature (heating threshold) | | `high` | number | Yes | Maximum temperature (cooling threshold) | | `scale` | string | No | `"C"` (default) or `"F"` | ```json theme={null} { "low": 68, "high": 74, "scale": "F" } ``` `low` must be less than `high`. Most thermostats require at least 2-3°F (1-2°C) difference. ## Response Success (200 OK): ```json theme={null} { "success": true, "message": "Command handled", "device": "02AA01AB01234567", "object": "shared.02AA01AB01234567", "revision": 154, "timestamp": 1764026400108 } ``` ## Code Examples ```bash cURL theme={null} curl -X POST https://nolongerevil.com/api/v1/thermostat/dev_abc123xyz/temperature/range \ -H "Authorization: Bearer nle_your_api_key_here" \ -H "Content-Type: application/json" \ -d '{"low": 68, "high": 74, "scale": "F"}' ``` ```javascript JavaScript theme={null} async function setTemperatureRange(deviceId, low, high, scale = 'F') { const response = await fetch( `https://nolongerevil.com/api/v1/thermostat/${deviceId}/temperature/range`, { method: 'POST', headers: { 'Authorization': 'Bearer nle_your_api_key_here', 'Content-Type': 'application/json' }, body: JSON.stringify({ low, high, scale }) } ); return await response.json(); } // Usage await setTemperatureRange('dev_abc123xyz', 68, 74, 'F'); ``` ```python Python theme={null} def set_temperature_range(device_id, low, high, scale='F'): response = requests.post( f'https://nolongerevil.com/api/v1/thermostat/{device_id}/temperature/range', headers={'Authorization': 'Bearer nle_your_api_key_here'}, json={'low': low, 'high': high, 'scale': scale} ) response.raise_for_status() return response.json() ``` ## Use Cases ### Comfort Range ```javascript theme={null} // Maintain 68-74°F for optimal comfort await setTemperatureRange(deviceId, 68, 74, 'F'); ``` ### Energy Efficiency ```javascript theme={null} // Wider range = less HVAC cycling = energy savings await setTemperatureRange(deviceId, 65, 78, 'F'); ``` ## Next Steps Switch to heat-cool mode Set single temperature # Choose Your Path Source: https://docs.nolongerevil.com/choose-your-path Hosted service or self-hosted? Pick the right setup for you ## Two Ways to Use No Longer Evil No Longer Evil offers two deployment options depending on your technical expertise and preferences: **Recommended for most users** Plug and play - no server setup required **For advanced users** Complete control over your infrastructure ## Hosted Service (Recommended) ### What You Get The hosted service uses No Longer Evil's managed infrastructure at **nolongerevil.com**. Your thermostat connects to our servers instead of Google's. No server setup, no database management, no SSL certificates. Just flash the firmware and start using your thermostat. Automatic updates to the API server, dashboard, and features. You always get the latest improvements without lifting a finger. Access your thermostat from anywhere via our hosted web dashboard. Control temperature, schedules, and settings from any device. Simple entry code system to link multiple thermostats to your account. Perfect for multi-zone setups. No technical expertise required. If you can follow step-by-step instructions and plug in a USB cable, you can do this. ### What You Need **15-30 minutes** Most of this is waiting for firmware to flash and boot **Beginner Friendly** Can follow simple instructions ### Limitations **Data Privacy**: Your thermostat data (temperature settings, schedules) is stored on No Longer Evil servers. We're committed to privacy, but if you need absolute data ownership, consider self-hosting. **Service Dependency**: Your thermostat depends on No Longer Evil's service availability. If our servers go down (rare), you'll temporarily lose remote access. Local thermostat functionality continues to work. ### Best For * Homeowners who want a simple, working solution * Users without technical/server management experience * Anyone who wants to try No Longer Evil without commitment Follow our step-by-step guide to flash firmware and start using your thermostat in under 30 minutes *** ## Self-Hosted (Advanced) ### What You Get Self-hosting means running your own No Longer Evil infrastructure. Your thermostat connects to **your server**, not ours. All thermostat data stays on your servers. You control the database, logs, and every piece of information. Your thermostat works independently of No Longer Evil's service. Even if we shut down, your setup keeps running forever. Modify the API server, build custom integrations, add features, or tweak behavior to your exact needs. Run everything on your local network for ultra-low latency and no internet dependency (optional). ### What You Need **30 minutes – 2 hours** Depends on your setup (Docker install, network config) **Advanced** Comfortable with command line, Docker, networking ### Requirements A machine that can run 24/7 (Raspberry Pi, NAS, VPS, home server, or cloud VM) * Command line / terminal navigation * Basic networking knowledge (local IPs, port forwarding if remote access is needed) * Docker experience (or willingness to learn) * Public IP or DDNS service (only if accessing from outside your home network) * Port forwarding capability on router (only if accessing remotely) ### Components You'll Run When self-hosting, you're responsible for running and maintaining: 1. **API Server (Port 8000)** — Handles thermostat device communication 2. **Control API / Dashboard (Port 8082)** — Web interface for device management 3. **MQTT (Optional)** — Publishes climate entities to Home Assistant via MQTT discovery ### Challenges to Expect **This path requires some technical comfort.** You'll need to: * Install and run Docker (or use the Home Assistant add-on) * Set a LAN IP in your server configuration * Keep a machine running 24/7 on your network * Pull updates manually when new versions release SSL certificates, firewalls, and databases are **not required** for a standard home LAN setup — these only apply if you're exposing your server to the internet. If you're not comfortable with Docker basics, we recommend the hosted service. Ready for the challenge? Follow our comprehensive guide to set up your own No Longer Evil infrastructure # Device Compatibility Source: https://docs.nolongerevil.com/compatibility Check if your Nest Thermostat is compatible with No Longer Evil ## Supported Devices **Fully Supported** Requires device teardown **Fully Supported** Simple USB connection **In Development** Not currently supported **Both Generation 1 and Generation 2 thermostats are fully supported.** Gen 2 devices are easier to flash (simple USB connection), while Gen 1 requires device disassembly and contact bridging to enter DFU mode. ## How to Identify Your Nest Thermostat Not sure which generation you have? Use this guide to identify your Nest Thermostat model. ### Visual Identification Here's what each generation looks like: **Key Features:** * Rotating ring with metallic finish * 8 wire connectors on backplate * Yellow/Green level bubble **Key Features:** * Similar rotating ring design * 8-10 wire connectors on backplate * Yellow/Green level bubble ### Generation 1 Thermostat Nest Generation 1 Thermostat ### Generation 2 Thermostat Nest Generation 2 Thermostat ### Method 1: Check the Backplate Remove your Nest from the wall mount and look at the backplate for these identifying features: **Yellow/Green** → Gen 1 or Gen 2 ✅ **Blue** → Gen 3 ❌ **8 connectors** → Gen 1 ✅ **8-10 connectors** → Gen 2 ✅ **10 connectors** → Gen 3 ❌ ### Method 2: Check Settings Menu This is the most reliable method: Press on your Nest display and navigate to **Settings** Navigate to **Settings → Technical Info** Look for the "Display model" field. The first digit tells you the generation: * **1x** (e.g., 1.12) → Generation 1 ✅ * **2x** (e.g., 2.8) → Generation 2 ✅ * **3x** (e.g., 3.7) → Generation 3 ❌ **Wondering why No Longer Evil exists?** Learn about [how Google is intentionally blocking older Nest devices](/why-google-blocks) even though they're fully functional. ## Generation-Specific Installation Requirements **Gen 1 thermostats are fully supported** but require device disassembly to enter DFU mode. You'll need: * Phillips screwdriver or similar tool * Metal tweezers or similar conductive object * Patience for careful disassembly Gen 1 installation involves opening the device, removing the battery, bridging specific contacts, and reconnecting while the firmware loader runs. Full teardown instructions are provided in the installation guide. Gen 1 installation is more complex and requires physical device modification. Only proceed if you're comfortable with electronics teardown. **Gen 2 thermostats are the easiest to flash** - just connect via USB and run the installer. You'll need: * Micro USB cable * No disassembly required Gen 2 is the recommended option for beginners due to its simple plug-and-play installation process. **Support is in development.** Generation 3 uses different hardware architecture and will require more extensive development. If you have Gen 3 hardware and want to help with development, please reach out via [GitHub](https://github.com/codykociemba/NoLongerEvil-Thermostat) or [Discord](https://discord.gg/AffeGaJ5XS). ## Additional Requirements Beyond having a Generation 1 or Generation 2 thermostat, you'll need: **Operating System**: Linux (recommended) or macOS **Purpose**: To run the firmware installer ⚠️ Windows is NOT supported (including WSL) **Type**: Mini USB (Gen 1) or Micro USB (Gen 2) **Purpose**: Connect thermostat to computer for firmware flashing **Level**: At least 50% charge **Purpose**: Ensure device stays powered during flashing **Speed**: Standard broadband **Purpose**: Download firmware & access dashboard ## Next Steps Decide between hosted service (easy) or self-hosted (advanced) Jump straight to the recommended hosted setup guide # Claiming Additional Devices Source: https://docs.nolongerevil.com/hosted/claiming-device Link multiple Nest thermostats to your account ## Overview You can link multiple Nest thermostats to a single No Longer Evil account. This is perfect for: * Multi-zone homes with multiple thermostats * Managing thermostats in different properties * Testing devices before deploying to production ## Prerequisites Before claiming an additional device: * [ ] Device has No Longer Evil firmware installed ([see installation guide](/hosted/installation)) * [ ] Device is connected to WiFi * [ ] You're logged into your No Longer Evil account ## How to Claim a Device On the Nest thermostat you want to link: 1. Navigate to **Settings → Nest App → Get Entry Code** 2. A 7-character code will appear (e.g., `K2P9WXL`) 3. Note the code (it expires in 1 hour) Go to [nolongerevil.com](https://nolongerevil.com) and log in to your account. In the dashboard: 1. Click **"Add Device"** or the **"+"** button 2. Enter the 7-character entry code 3. Click **"Claim Device"** If successful, the device will appear in your dashboard immediately with: * Device serial number * Current temperature * Connection status You can now control it from the dashboard! ## Entry Code Details ### How Entry Codes Work Entry codes are temporary 7-character strings that link a device to your account: * **Generated on-demand** by the thermostat * **Single-use** - each code can only claim one device * **Time-limited** - expires after 1 hour (3600 seconds by default) * **Random** - uses secure random generation * **Case-sensitive** - must be entered exactly as shown ### Why Entry Codes? Entry codes provide a secure way to link devices without requiring complex authentication on the thermostat: 1. **User-friendly**: Easy to type and read 2. **Secure**: Short-lived and single-use prevents unauthorized access 3. **Offline-capable**: Can be generated without internet (code validated when connected) 4. **No credentials needed**: Don't need to enter passwords on the thermostat ## Managing Multiple Devices Once you have multiple devices linked: ### Viewing All Devices The dashboard shows all your devices in a grid or list view: * Device name (customizable) * Current temperature * Target temperature * Heating/cooling status * Last seen timestamp ### Renaming Devices Click the gear icon or settings button on a device card Enter a friendly name like: * "Living Room" * "Master Bedroom" * "Guest House" * "Basement" Click Save - the new name appears everywhere in the dashboard ### Removing Devices To unlink a device from your account: Open the device settings menu Click **"Remove Device"** or **"Unlink"** Confirm the removal The device will continue to function as a local thermostat, but you'll lose remote access until you claim it again. ## Troubleshooting **Possible causes:** * Code was mistyped (check for 0/O, 1/I/L confusion) * Code expired (>1 hour old) - generate a new one * Code already used - generate a new one * Device not connected to WiFi - check network **Solution**: Generate a fresh entry code and try again Entry codes are only valid for 1 hour after generation. **Solution**: Generate a new code: * Settings → Nest App → Get Entry Code * Enter the new code immediately The device may not be connected to WiFi or can't reach No Longer Evil servers. **Check:** * WiFi connection on the thermostat (Settings → Network) * Internet connectivity * Firewall not blocking port 443 See [Network Troubleshooting](/hosted/troubleshooting#network-issues) Make sure: * No Longer Evil firmware is installed (see the No Longer Evil logo on boot) * Device has completed first boot * You're in Settings → Nest App (not Settings → App) If still missing, the firmware may not have flashed correctly. Try reflashing. ## Security Considerations **Physical access required**: To claim a device, someone must have physical access to the thermostat to retrieve the entry code. This prevents remote hijacking of devices. **Don't share entry codes**: Treat entry codes like passwords. Anyone with the code can link the device to their account. If you accidentally share a code, let it expire or claim the device yourself immediately. ## Next Steps Learn how to control your thermostats Common issues and solutions # Installation Guide Source: https://docs.nolongerevil.com/hosted/installation Step-by-step instructions to flash firmware and set up your Nest thermostat **Before you begin:** * Ensure you have a **Nest Generation 1 or Generation 2** thermostat ([check compatibility](/compatibility)) * **Gen 1 requires device teardown** - only proceed if comfortable with electronics disassembly * **Charge your Nest first** — If the device hasn't been used in a while, plug it into USB and let it charge for at least 30 minutes before flashing. A low battery can cause the flash to fail or the device to shut off mid-process. ## What You'll Need Nest Generation 1 or 2 thermostat with at least 50% battery charge [Check compatibility →](/compatibility) Linux or macOS only ❌ Windows is NOT supported — see below Mini USB (Gen 1) or Micro USB (Gen 2) Must be data-capable, not charge-only For Gen 1 Only Metal tweezers/conductive tool *** ## Step 1: Download Firmware Installer **Windows is not supported.** The Nest thermostat only remains in DFU (flash) mode for a few seconds after USB connection. Windows requires too long to install the USB bootloader driver, making it impossible to flash in time. This is a hardware timing constraint — WSL2, virtual machines, and other workarounds do not solve it. **You must use macOS or Linux to flash your thermostat.** Download the firmware installer for your operating system: **macOS (Intel x64)** Intel Macs Click to download → **macOS (Apple Silicon)** M1/M2/M3/M4 Macs Click to download → **Linux (x64)** Ubuntu, Debian, Fedora, etc. Click to download → *** ## Step 2: Prepare the Installer 1. **Install dependencies**: ```bash theme={null} sudo apt-get update sudo apt-get install -y unzip curl build-essential libusb-1.0-0-dev libudev-dev gcc pkg-config curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - sudo apt-get install -y nodejs ``` 2. **Extract the downloaded .zip file**: ```bash theme={null} unzip nolongerevil-linux-appimage.zip ``` 3. **Make the AppImage executable**: ```bash theme={null} chmod +x nolongerevil-*.AppImage ``` 4. **Add USB device permissions** (Optional, but recommended): ```bash theme={null} echo 'SUBSYSTEM=="usb", ATTR{idVendor}=="0451", MODE="0666"' | sudo tee /etc/udev/rules.d/99-omap.rules sudo udevadm control --reload-rules ``` If you skip this and hit a "permission denied" error later, come back and run these, then retry. 5. **Run the AppImage**: ```bash theme={null} ./nolongerevil-*.AppImage ``` **The installer GUI will open and walk you through the rest of the process.** You can follow the on-screen wizard instead of reading the steps below. 1. **Extract the downloaded .zip file**: * Double-click the downloaded `.zip` in Finder (`nolongerevil-macos-x64.zip` or `nolongerevil-macos-arm64.zip`) 2. **Open the app**: * Double-click NoLongerEvil Installer.app * If macOS blocks it, right-click → Open, then click Open again **First time opening**: macOS may show a security warning. Go to System Preferences → Security & Privacy and click "Open Anyway" if needed. **The installer GUI will open and walk you through the rest of the process.** You can follow the on-screen wizard instead of reading the steps below. If the pre-built installer doesn't work for you, you can run it manually from source: ```bash theme={null} # Install dependencies sudo apt-get update sudo apt-get install -y curl build-essential libusb-1.0-0-dev libudev-dev gcc pkg-config # Install Node.js 20 (required) curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - sudo apt-get install -y nodejs # Clone the repository git clone https://github.com/codykociemba/NoLongerEvil-Thermostat.git NoLongerEvil-Thermostat # Run the installer cd NoLongerEvil-Thermostat/firmware/installer npm install npm run electron:dev ``` ```bash theme={null} # Install Homebrew if not already installed /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" # Install Node.js 20 (required) brew install node@20 brew link --overwrite node@20 # Clone the repository git clone https://github.com/codykociemba/NoLongerEvil-Thermostat.git NoLongerEvil-Thermostat # Run the installer cd NoLongerEvil-Thermostat/firmware/installer npm install npm run electron:dev ``` This will run the installer directly from source using Electron. The installer GUI will open just like the pre-built version. *** ## Step 3: Enter DFU Mode When you launch the installer, it will first run a system check and then ask you to select your device generation: System Check — installer verifying prerequisites **System Check** — The installer verifies that required dependencies are present before proceeding. **If it fails:** * **libusb not found** — You skipped the dependency install step. Run `sudo apt-get install -y libusb-1.0-0-dev libudev-dev` (Linux) or `brew install libusb` (macOS), then relaunch the installer. * **Permission error / USB access denied** — On Linux, add the udev rule from Step 2 and relaunch. On macOS, grant USB access in System Preferences → Security & Privacy. * **On macOS: "app is damaged" or won't open** — Right-click → Open, then click Open in the dialog. Or go to System Preferences → Security & Privacy → General → click "Open Anyway". Select Generation — choose Gen 1 or Gen 2 **Select Generation** — Choose whether your thermostat is a Generation 1 or Generation 2 device. If you're unsure, check the [compatibility page](/compatibility). Selecting the wrong generation will flash incorrect firmware — if this happens, simply re-run the installer and select the correct option. Custom Firmware option for advanced users **Why use custom firmware?** There are a few reasons you might want to build your own: * **Self-hosted server URL** — The default firmware points to the No Longer Evil hosted service. If you're self-hosting and want the thermostat to connect to your server out of the box — without needing the wizard — you can bake your server's URL directly into the firmware. * **Custom root filesystem** — The firmware builder gives you access to the full root filesystem. You can add, remove, or modify scripts and files before the image is built, then flash that customized image to all of your Nest devices consistently. To build your own firmware, clone the repo and use the Docker-based builder: ```bash theme={null} git clone https://github.com/codykociemba/NoLongerEvil-Thermostat.git cd NoLongerEvil-Thermostat/firmware/builder ./docker-build.sh --generation both --enable-root-access --yes ``` Copy the output into the installer before running it: ```bash theme={null} cp firmware/builder/firmware/* firmware/installer/resources/firmware/ ``` Then point the installer at your custom firmware using the option shown above. The process differs between Generation 1 and Generation 2 devices. **Choose the instructions for your device:** ### Gen 2: Simple Reboot Method Follow these steps carefully: 1. **Attach to back plate** - Connect your Nest to the wall back plate and wait 2–3 seconds — this arms the DFU trigger 2. **Remove from back plate** - Remove the Nest from its back plate/wall mount 3. **Connect via USB immediately** - Plug the Nest into your computer using a micro USB cable 4. **Make sure the No Longer Evil installer app is open** and on the flashing screen, waiting for the device 5. **Reboot the device** - Press and hold down on the display for 10-15 seconds until the device reboots 6. **DFU mode active** - Once it reboots, the device will enter DFU mode and the installer will recognize it and begin flashing > **The back plate step is required every time.** The back plate contact arms the DFU trigger in the bootloader — without it, the device boots normally instead of entering flash mode. If the installer doesn't detect the device: unplug USB, re-seat on the back plate, wait 2–3 seconds, remove, plug USB again. Repeat the full sequence each attempt. The firmware installer will automatically detect the device and flash the custom bootloader (x-load, u-boot) and kernel (uImage). Firmware installing — installer progress screen **Flashing in progress** — The installer is writing the bootloader and kernel to the device. Do not unplug the USB cable until it completes. **If it stalls or fails:** Unplug USB, repeat the full back plate sequence (attach → 2–3 sec → remove → plug USB), and try again. If the installer never detects the device, check your USB cable (must be data-capable, not charge-only) and try a different USB port. ### Gen 1: Teardown & Contact Bridge Method **Gen 1 requires physical disassembly.** This process involves opening the device and manually bridging internal contacts. Only proceed if you're comfortable with electronics teardown. **Credit:** Gen 1 DFU mode instructions adapted from [guyman70718's cuckoo\_loader\_gen1 guide](https://github.com/guyman70718/cuckoo_loader_gen1). #### Tools Needed: * Phillips screwdriver or prying tool * Metal tweezers or similar conductive object * Mini USB cable * Non-conductive object for battery removal #### Teardown Steps: Follow the [SparkFun Nest Thermostat Teardown Guide](https://learn.sparkfun.com/tutorials/nest-thermostat-teardown-/all) to open your Gen 1 thermostat. You need to access the internal circuit board. Gen 1 Nest internal components exposed Unplug the battery by inserting your finger or a **NON-CONDUCTIVE object** underneath the battery wires. It lifts directly up. **Important:** Do not use metal tools for this step. Detach the metal shield from the corner to expose the circuit board components and contact points. Metal shield removal Reconnect the battery while keeping the shield removed. **Note:** The device will not boot without a battery connected. Ensure: * Battery is reconnected * Screen is off * The **NoLongerEvil installer app is open** and waiting on your computer * Mini USB cable is ready but not yet connected Using metal tweezers or a conductive tool, **bridge the black and yellow resistor contacts** shown in the image below: Contact bridging points for DFU mode While holding the contacts bridged, connect the mini USB cable to the device. The thermostat should boot into DFU mode and the installer will detect it. Once detected, the firmware installer will automatically flash the custom bootloader and kernel. Keep the device connected and don't disconnect until flashing completes. **After successful flashing**, you can reassemble your Gen 1 thermostat by reversing the teardown steps. The device should boot normally with the new firmware. Installation Complete screen **Firmware flashed successfully** — The bootloader and kernel have been written to the device. Keep the USB cable connected — the device will now begin booting into the new firmware. **If flashing failed mid-way:** Unplug and repeat the back plate sequence, then re-run the installer. A partial flash will not permanently damage the device — the DFU bootloader is always recoverable. *** ## Step 4: Wait for Boot Keep the device plugged in via USB and wait for it to complete its boot sequence — this may take 3–4 minutes. When fully booted, you should see the default Nest temperature screen. **Do not disconnect or power off the device during this time.** *** ## Step 5: Complete the Wizard **Once the device has booted, the installer wizard takes over automatically.** The wizard will guide you through: * Establishing an SSH connection to the thermostat * Configuring SSH access (disable or change the default password) * Discovering your Home Assistant instance (optional) * Discovering and registering your Nest device Follow the on-screen wizard prompts. Once the wizard finishes, continue to Steps 6 and 7 below to register your account and link your device to the dashboard. Hosting Mode selection — choose Cloud Hosted or Self-Hosted **Choose hosting mode** — Select **Cloud Hosted** to use the No Longer Evil managed service (recommended for most users). Select **Self-Hosted** only if you have already set up your own NLE server. If you haven't set up a server yet and pick Self-Hosted, the wizard will not be able to connect — go back and choose Cloud Hosted, or set up your server first. SSH Access configuration screen **SSH connection** — The wizard connects to the thermostat over SSH to configure it. The device must still be connected via USB and should have booted into the new firmware by now. **If SSH fails:** Make sure the USB cable is still plugged in and the device has finished booting (when fully booted, you should see the default Nest temperature screen). If you see a connection timeout, unplug and replug the USB cable, wait 30 seconds, and click Retry. If you choose to keep SSH enabled, this screen lets you change the default SSH password from `nolongerevil` to a password of your choice. This is recommended if your thermostat is on a network you don't fully control. You can SSH into the device at any time with `ssh root@[THERMOSTAT-IP]`. SSH Password fields for custom credentials Finding Your Nest on the network **Finding your Nest on the network** — The wizard scans your local network to locate the thermostat after it connects to WiFi. The device needs to be on the same network as your computer. **If the Nest isn't found:** Check the WiFi credentials on the Nest device itself and confirm it shows as connected. The scan may take up to 60 seconds — wait for it to complete before retrying. Alternatively, if you know the thermostat's IP address, you can enter it manually and click **Use**. If it still fails, see the [Troubleshooting guide](/hosted/troubleshooting). Configuring Device — provisioning in progress **Configuring device** — The wizard is writing the server URL and account settings to the thermostat. Do not unplug or power off the device during this step. **If provisioning fails:** Check your internet connection and try again. If it fails repeatedly, see the [Troubleshooting guide](/hosted/troubleshooting) for SSH-based manual configuration. Setup Complete **Setup complete** — The thermostat is configured and connected. Continue to Steps 6 and 7 below to register your account and link the device to your dashboard. *** ## Step 6: Register Account Once the device has fully rebooted: 1. Visit **[https://nolongerevil.com](https://nolongerevil.com)** in your web browser 2. **Register an account** (or sign in if you already have one) 3. Navigate to your **Dashboard** You will see a "No devices" screen that prompts you for an entry code. *** ## Step 7: Link Your Device To link your Nest device to your No Longer Evil account: 1. On your Nest device, navigate to: **Settings → Nest App → Get Entry Code** 2. The device will display a unique entry code 3. Enter this code on the No Longer Evil dashboard 4. Your device is now linked and ready to use! *** ## Step 8: Reinstall on Wall Once everything is working: 1. Align the Nest with the backplate on the wall 2. Push gently until it clicks into place 3. The device will reconnect to HVAC power and charge *** ## 🎉 Success! Your Nest Thermostat is now running No Longer Evil and connected to your account! *** ## Having trouble? See the [Troubleshooting guide →](/hosted/troubleshooting) *** ## Credits & Acknowledgments This project builds upon the excellent work of several security researchers and developers: * **[grant-h](https://github.com/grant-h) / [ajb142](https://github.com/ajb142)** - [omap\_loader](https://github.com/ajb142/omap_loader), the USB bootloader tool used to flash OMAP devices * **[exploiteers (GTVHacker)](https://github.com/exploiteers)** - Original research and development of the [Nest DFU attack](https://github.com/exploiteers/NestDFUAttack), which demonstrated the ability to flash custom firmware to Nest devices gen 1 & gen 2 * **[guyman70718](https://github.com/guyman70718)** - [cuckoo\_loader\_gen1](https://github.com/guyman70718/cuckoo_loader_gen1) guide and teardown documentation for Gen 1 DFU mode access * **[FULU](https://bounties.fulu.org/)** and all bounty backers - For funding the [Nest Learning Thermostat Gen 1/2 bounty](https://bounties.fulu.org/bounties/nest-learning-thermostat-gen-1-2) and supporting the right-to-repair movement Without their groundbreaking research, open-source contributions, and advocacy for device ownership rights, this work would not be possible. Thank you! *** Join our Discord community or open a GitHub issue for assistance # Hosted Service Overview Source: https://docs.nolongerevil.com/hosted/overview Get started with No Longer Evil in under 30 minutes ## What is the Hosted Service? The No Longer Evil hosted service is a **plug-and-play solution** that allows you to liberate your Nest Thermostat from Google's cloud without setting up your own server infrastructure. **Perfect for beginners!** If you can follow step-by-step instructions and plug in a USB cable, you can successfully set up No Longer Evil. ## How It Works Download the firmware installer and flash custom firmware to your Nest Gen 1 or Gen 2 thermostat via USB. The firmware modifies the network layer to redirect all traffic from Google servers to `backdoor.nolongerevil.com`. Register a free account at [nolongerevil.com](https://nolongerevil.com) to manage your devices. Use a 7-character entry code from your thermostat to link it to your account. Access your thermostat through the web dashboard from anywhere in the world! ## What You'll Need Confirmed compatible device [Check compatibility →](/compatibility) Linux or macOS (Windows/WSL not supported) Mini USB (Gen 1) or Micro USB (Gen 2) Gen 2: 15-30 min | Gen 1: 30-45 min (requires teardown) ## After Installation Once setup is complete, your thermostat will: * ✅ Connect to No Longer Evil servers instead of Google * ✅ Function normally for heating and cooling * ✅ Be accessible via the web dashboard at nolongerevil.com * ✅ Retain all its features (scheduling, auto-away, etc.) * ❌ No longer appear in the Google Nest app * ❌ No longer send data to Google servers ## Ready to Get Started? **Step-by-step instructions** to flash firmware and set up your thermostat Solutions to common issues during setup *** ## Prefer Complete Control? If you're comfortable with server administration and want complete data ownership: Host your own No Longer Evil infrastructure # Troubleshooting Source: https://docs.nolongerevil.com/hosted/troubleshooting Solutions to common issues with hosted service setup ## DFU Mode Issues **Symptoms**: Holding display doesn't trigger installer detection **Solutions**: 1. **IMPORTANT - Boot device on backplate first** (Gen 2): * Connect device to wall backplate and let it fully boot * Wait until display shows normal home screen * Then remove from backplate to begin DFU process * This ensures battery is properly charged and device is ready 2. **Charge the battery**: DFU mode requires sufficient power (>50%) * Check: Settings → Technical Info → Battery * Charge on wall mount for 1-2 hours if low 3. **Try a different hold duration**: * Some devices need 10 seconds, others 15 * Try holding for 20 seconds to be safe 4. **Ensure installer is running FIRST**: * Start the installer app BEFORE rebooting * Wait for "Waiting for device..." message 5. **Try a different USB cable**: * Some cables are charging-only (no data pins) * Use a cable that works for file transfer 6. **Try a different USB port**: * Some ports don't provide enough power * Try a USB 2.0 port instead of USB 3.0 **Symptoms**: Thermostat reboots but installer still says "Waiting..." **Solutions**: 1. **Grant USB permissions** (macOS): * System Preferences → Security & Privacy * Allow No Longer Evil app USB access 2. **Run with sudo** (Linux): ```bash theme={null} sudo ./NoLongerEvil-*.AppImage ``` 3. **Add udev rules** (Linux): ```bash theme={null} echo 'SUBSYSTEM=="usb", ATTR{idVendor}=="0451", MODE="0666"' | sudo tee /etc/udev/rules.d/99-omap.rules sudo udevadm control --reload-rules ``` 4. **Try a different computer**: * Some computers have USB compatibility issues * Ensure you're using Linux or macOS (Windows/WSL not supported) * Try a different OS if possible **Symptoms**: Device enters DFU but flashing fails repeatedly **Solutions**: 1. **Let device fully boot first**: * After failed attempt, let device boot completely * Wait 5 minutes * Try DFU mode again 2. **Check USB connection stability**: * Wiggle cable to check for loose connection * Try a different cable and port 3. **Close other USB software**: * Close iTunes, Android tools, other device managers * These can interfere with USB communication 4. **Restart computer**: * Sometimes USB stack gets stuck * Fresh boot can resolve issues *** ## Device Detection **Check these in order**: 1. **USB cable is data-capable** * Test cable with phone file transfer * If phone only charges, cable is power-only 2. **Device is actually in DFU mode** * Screen should be blank or flickering * Normal boot screen means DFU wasn't entered 3. **USB port has power** * Try different ports * Desktop back panel ports often more reliable than front 4. **OS permissions**: * **macOS**: Security & Privacy → allow USB * **Linux**: Run with sudo or add udev rules **Symptoms**: Installer briefly detects device then loses connection **Causes**: * Loose USB connection * Insufficient power * USB cable failure **Solutions**: 1. Use high-quality, short USB cable 2. Connect to computer directly (not through hub) 3. Try different USB port 4. Check for bent pins in USB connector *** ## Flashing Issues **Symptoms**: Progress bar stops, error message appears **Solutions**: 1. **Don't move the device**: * Keep thermostat and cable perfectly still during flashing * Even small movements can interrupt USB connection 2. **Check free disk space**: * Ensure computer has >1GB free space * Firmware files are extracted temporarily 3. **Disable antivirus temporarily**: * Some antivirus software blocks low-level USB access * Whitelist No Longer Evil installer 4. **Download fresh firmware**: * Corrupt download can cause failures * Delete and redownload installer **Symptoms**: "Firmware verification failed" after flashing **Causes**: * Corrupted firmware files * USB communication error * Device memory issues **Solutions**: 1. Try flashing again (sometimes works on 2nd attempt) 2. Download fresh installer 3. Try different computer 4. If persistent, device may have hardware issues *** ## Post-Flash Wizard Issues **Symptoms**: Wizard reports "Could not connect via SSH" or hangs after flashing **Solutions**: 1. **Keep USB connected** — SSH runs over the USB interface during wizard setup 2. **Wait longer** — The device needs 3–4 minutes to fully boot before SSH is available 3. **Unplug and re-plug the USB cable** — Sometimes the USB network interface needs a reset 4. **Check USB cable is data-capable** — Charge-only cables won't carry SSH traffic 5. **Restart the installer** and try again from the beginning **Symptoms**: Wizard reports "No Home Assistant instance detected" **Solutions**: 1. **Skip if not using HA** — Home Assistant discovery is optional; you can skip this step 2. **Verify HA is running** and reachable on your local network 3. **Check same network/VLAN** — Your computer and HA must be on the same subnet 4. **Enter manually** — If auto-discovery fails, type your HA URL (e.g., `http://192.168.1.x:8123`) directly when prompted **Symptoms**: Wizard reports "No Nest device found" or device discovery times out **Solutions**: 1. **Verify WiFi is connected** on the thermostat: Settings → Network 2. **Same network required** — The thermostat and your computer must be on the same local network 3. **Wait for full boot** — The thermostat must show the No Longer Evil logo before it's discoverable 4. **Reboot the thermostat** — Hold display 10 seconds to power off, then reconnect USB and wait for boot 5. **Check firewall** — Ensure nothing is blocking local network discovery (mDNS / UDP) **Symptoms**: "Configuration failed" error at the final wizard step **Solutions**: 1. **Check the installer log** — Scroll up in the installer output for a specific error message 2. **Verify server address** — If self-hosted, confirm your server IP and port are correct: * Standalone Docker: port `8000` * HA Add-on: port `9543` 3. **Check port access** — Ensure the thermostat can reach your server on the correct ports 4. **Restart the installer** and repeat from Step 3 — most configuration errors clear on retry 5. **Post to Discord** with your installer log output for help **Symptoms**: Device appears in the dashboard but temperature/mode don't update **Solutions**: 1. **Reboot the thermostat** — Hold the display for 10 seconds. The thermostat only sends full state on a fresh boot 2. **Check server logs** — `docker logs nolongerevil-server --tail 50` — look for subscription activity 3. **Factory reset** may be required if the device was previously paired to a different server *** ## Boot Issues **Symptoms**: Screen stays blank after firmware flash **Don't panic!** Try these: 1. **Wait longer**: * First boot takes 3-4 minutes * Leave plugged in via USB for 5+ minutes 2. **Try power cycle**: * Unplug USB * Hold display for 10 seconds (power off) * Reconnect USB * Wait for boot 3. **Reflash firmware**: * Enter DFU mode again * Reflash with installer * Multiple flashes sometimes needed 4. **Check for signs of life**: * Screen backlight flickers (good sign) * Device gets slightly warm (CPU active) If device is completely unresponsive after multiple reflash attempts and no backlight activity, it may be bricked. Contact support before giving up - there may be recovery options. **Symptoms**: Boot text with errors, or boot loop **Solutions**: 1. Let it fully boot - errors may be non-critical 2. If boot loop persists: * Enter DFU mode * Reflash firmware 3. Take photo of error messages and post to Discord/GitHub **Symptoms**: Device boots but no custom logo This is **not critical** - the firmware may still work: 1. Check if device has network connectivity 2. Try generating an entry code 3. If entry code works, firmware is installed correctly * Logo may not display due to screen driver differences *** ## Entry Code Issues **Symptoms**: Can't find Settings → Nest App → Get Entry Code **Causes**: * Firmware didn't flash correctly * Looking in wrong menu **Solutions**: 1. **Check correct path**: * Settings → **Nest App** → Get Entry Code * Not Settings → App Info 2. **Verify firmware installed**: * Check if No Longer Evil logo showed on boot * Settings → Technical Info → look for modified fields 3. **Reflash if needed**: * If firmware not installed, reflash * Follow installation guide from Step 1 **Symptoms**: Dashboard says "Invalid code" or "Expired" **Solutions**: 1. **Check code carefully**: * Entry codes are case-sensitive * Common mistakes: 0/O, 1/I/L, 2/Z, 5/S 2. **Check expiration**: * Codes expire after 1 hour * Generate a fresh code 3. **Verify device is online**: * Settings → Network * Ensure WiFi connected * Check for internet access 4. **Check firewall**: * Device needs to reach backdoor.nolongerevil.com:443 * Corporate/school networks may block 5. **Try different network**: * Connect device to phone hotspot temporarily * If works, network is the issue **Symptoms**: "This code has already been claimed" **Cause**: Entry codes are single-use **Solution**: Generate a new entry code *** ## Network Issues **Symptoms**: Can't join network or shows "No connection" **Solutions**: 1. **Check WiFi password**: * Re-enter carefully (case-sensitive) * Use guest network if available (simpler password) 2. **Check WiFi band**: * Nest Gen 2 only supports 2.4 GHz * Disable 5 GHz or separate networks 3. **Move closer to router**: * Weak signal can prevent connection * Check signal strength: Settings → Network 4. **Restart router**: * Sometimes helps with DHCP issues 5. **Check MAC filtering**: * If router has MAC filtering, add thermostat MAC * Find MAC: Settings → Technical Info **Symptoms**: WiFi connected but dashboard says "Offline" **Causes**: * Firewall blocking port 443 * DNS issues * Network blocking HTTPS to unknown servers **Solutions**: 1. **Check firewall**: * Allow outbound HTTPS (port 443) * Whitelist backdoor.nolongerevil.com 2. **Test DNS resolution**: * On another device, ping backdoor.nolongerevil.com * If fails, DNS issue 3. **Try different network**: * Use phone hotspot to isolate issue * If works on hotspot, network is blocking 4. **Check for captive portal**: * Some networks require browser login * Connect different device first, then thermostat 5. **Contact network admin**: * Corporate/school networks often restrictive * May need whitelist exception *** ## Dashboard Issues **Symptoms**: Thermostat controls HVAC but dashboard shows offline **Likely cause**: Thermostat can't reach No Longer Evil servers **Solutions**: * See [Network Issues](#network-issues) above * Check router firewall rules * Verify internet connectivity on thermostat **Symptoms**: Changes on dashboard don't reflect on device (or vice versa) **Solutions**: 1. **Check connection status**: Device must show "Connected" 2. **Refresh dashboard**: Hard refresh (Ctrl+F5 / Cmd+Shift+R) 3. **Wait up to 30 seconds**: Long-polling interval 4. **Check for errors**: Browser console (F12) for JavaScript errors **Symptoms**: Logged in but device list is empty **Solutions**: 1. **Verify device was claimed**: * Check for "Device linked successfully" message * Try claiming again with new entry code 2. **Refresh page**: Sometimes takes a moment to populate 3. **Check browser console**: * F12 → Console tab * Look for API errors 4. **Try different browser**: Test in incognito/private mode 5. **Clear cache**: Cookies/cache corruption can cause issues *** ## Still Need Help? If none of these solutions work: Ask the community - often fastest help Report bugs or request features # Introduction Source: https://docs.nolongerevil.com/introduction Take back control of your Nest Thermostat with No Longer Evil **⚠️ EXPERIMENTAL SOFTWARE** This project is currently in the **experimental/testing phase**. Do NOT use this firmware on any thermostat that is critical for your heating or cooling needs. Flashing this firmware may brick your device or cause unexpected behavior. Only proceed if you have a backup thermostat or can afford to have your device non-functional during testing. ## What is No Longer Evil? No Longer Evil is a right-to-repair firmware and cloud replacement for Nest Thermostats that **frees your device from Google's cloud dependency**. By flashing custom firmware, your thermostat will operate independently and connect to No Longer Evil's platform (or your own self-hosted server), giving you complete control over your device data and settings. ## How It Works The custom firmware works by intercepting the communication layer between your thermostat and Google's servers: 1. **Firmware Replacement**: We flash modified bootloader and kernel components to your Nest device 2. **Traffic Redirection**: All network traffic is redirected from Nest/Google servers to the No Longer Evil API (Hosted or Self Hosted) 3. **API Compatibility**: Our server hosts a reverse-engineered replica of Google's API 4. **Seamless Operation**: The thermostat believes it's communicating with official Nest infrastructure 5. **Full Control**: You gain complete control through our web dashboard or API The thermostat's existing software remains intact and functional. We only modify the network layer to break free from Google's cloud dependency while maintaining full compatibility. ## What You Need Before getting started, make sure you have: Nest Generation 1 or 2 Thermostat (Gen 3 In Development) Linux or macOS for firmware flashing Mini USB (Gen 1) or Micro USB (Gen 2) cable For downloading firmware and accessing the dashboard ## Next Steps Verify your Nest Thermostat is Generation 1 or 2 and learn how to identify your device # Authentication Source: https://docs.nolongerevil.com/nest-protocol/authentication HTTP Basic Auth, device identification, and entry key pairing ## Overview The Nest protocol uses HTTP Basic Authentication on every request. The device embeds its serial number in the user ID field, allowing the server to identify any device from any request. This page covers **device-level authentication** on the Device Protocol API (port 8000). For the Control API (port 8082), see [API Authentication](/api-reference/authentication). *** ## HTTP Basic Auth Every subscribe, PUT, and entry request includes: ``` Authorization: Basic ``` The **user ID** follows the format `d.{SERIAL}.{suffix}`: ``` Authorization: Basic ^^^^^^^^^^^^^^^^^ serial number extracted here ``` To identify the device, decode the Base64 value and extract the second dot-delimited segment. ### Identity header fallback Some non-production firmware configurations send `X-nl-client-id` or `X-nl-device-id` headers instead of Basic Auth. On production firmware, Basic Auth is **always** present. Handle headers as fallback only: | Header | Format | When sent | | ---------------------- | ---------------------------------- | ----------------------------------- | | `Authorization: Basic` | `base64(d.{SERIAL}.{suffix}:pass)` | Always (production firmware) | | `X-nl-client-id` | `d.{SERIAL}.{suffix}` | Non-production only | | `X-nl-device-id` | `{SERIAL}` (bare serial) | Non-production only, entry requests | *** ## Credential Provisioning The server can provision new credentials to the device by including `X-nl-set-client-credentials` in any 200 response: ```http theme={null} X-nl-set-client-credentials: userid password ``` The device stores these and uses them for subsequent requests. **Do not provision credentials via 401 responses.** This can cause a credential loop on some firmware versions: the device receives new credentials, falls back to defaults before using them, gets another 401, receives credentials again — indefinitely. If you provision credentials, do it only in **200 responses**. ### Recommended approach for NLE The NLE self-hosted server skips credential provisioning entirely: 1. Accept all subscribe and PUT requests regardless of password 2. Parse the device serial from the Basic Auth user ID 3. Look up the device in the database by serial This avoids credential management complexity and the loop described above. *** ## Entry Key (Pairing Code) During device setup, users must enter a code displayed on the thermostat to claim it. The device fetches this code from the `passphrase_url` returned by `/nest/entry`. ### Request ```http theme={null} GET /nest/passphrase HTTP/1.1 Authorization: Basic ``` ### Response ```json theme={null} { "value": "A3XR7M2", "expires": 1707234600000 } ``` | Field | Type | Notes | | --------- | ---------- | ------------------------------------------------------------------ | | `value` | string | 7-character alphanumeric code | | `expires` | **number** | Milliseconds since epoch — **must be a JSON number, not a string** | If `expires` is a JSON string (e.g., `"1707234600000"` with quotes), the device silently rejects the response and never displays the entry code. Use a number literal. The device displays the code in `XXX-XXXX` format (e.g., `A3X-R7M2`). The expiration must be at least 30 minutes in the future. The server returns the **same** unexpired key on repeated calls — generating a new key on each poll would invalidate the code before the user can enter it. *** ## Pairing Completion After the user enters the entry code, the server completes pairing by pushing two buckets to the device on its open subscribe connection: **1. User bucket** — triggers pairing on the device: ```json theme={null} { "object_revision": 1, "object_timestamp": 1707148800000, "object_key": "user.homeassistant", "value": { "name": "homeassistant" } } ``` The `name` field is what triggers pairing completion internally. Without it, the setup screen remains visible. **2. Structure bucket** — establishes the device-home association: ```json theme={null} { "object_revision": 1, "object_timestamp": 1707148800000, "object_key": "structure.default", "value": { "name": "Home", "devices": ["09AA01AB12345678"] } } ``` Include both the user bucket and structure bucket on **every subscribe reconnection** for paired devices — not just at registration time. If the server restarts or the device reboots, the device re-registers and needs these buckets to restore pairing state. # device bucket Source: https://docs.nolongerevil.com/nest-protocol/bucket-device The largest bucket — device telemetry, configuration, hardware state, and sensor readings ## Overview The `device` bucket is the most complex, with 239 registered fields. It contains a mixture of device telemetry (read-only), cloud-writable configuration, and special-purpose fields. **Object key:** `device.{serial}`\ **Direction:** Bidirectional (with per-field restrictions)\ **Revision type:** `base_object_revision` (unconditional) *** ## Field Access Modes Not all fields in the device bucket work the same way: | Mode | Count | Server can write? | Description | | ------------------ | ----- | ---------------------- | ---------------------------------------------- | | **Device-only** | 113 | No — device overwrites | Hardware state, sensors, computed values | | **Cloud-writable** | 103 | Yes | Configuration the server can push | | **Special** | 23 | Varies | Custom processing paths (eco, HVAC capacities) | If you push a device-only field, the device compares it against its local value. If different, the device marks the field dirty and re-sends its own value in the next PUT — **the device always wins**. Accept these re-PUTs normally. *** ## Key Read-Only Fields These are the most important device-only fields. The server stores them (from device PUTs) but must not push them back. | Field | Type | Description | | ------------------------- | ------- | -------------------------------------------------------------- | | `current_temperature` | float | Indoor temperature in °C *(note: actually in `shared` bucket)* | | `current_humidity` | integer | Relative humidity (%) | | `backplate_temperature` | float | Backplate sensor temperature (°C) | | `battery_level` | float | Battery voltage level | | `has_fan` | boolean | Fan wiring detected | | `has_humidifier` | boolean | Humidifier wiring detected | | `has_dehumidifier` | boolean | Dehumidifier wiring detected | | `has_emer_heat` | boolean | Emergency/auxiliary heat wiring detected | | `has_hot_water_control` | boolean | Hot water system detected (UK/EU) | | `can_heat` | boolean | Device can heat *(actually in `shared` bucket)* | | `can_cool` | boolean | Device can cool *(actually in `shared` bucket)* | | `time_to_target` | integer | Estimated seconds until target temperature | | `time_to_target_training` | string | Training status: `"ready"`, `"training"`, or `"not_ready"` | | `error_code` | string | Active error code | | `serial_number` | string | Device serial number | | `current_version` | string | Firmware version | | `model_version` | string | Hardware model | | `local_ip` | string | Device IP on local network | | `mac_address` | string | WiFi MAC address | | `leaf` | boolean | Nest Leaf icon displayed | | `auto_away` | integer | Occupancy sensor: `0` = home, `1` = away | *** ## Cloud-Writable Fields These fields accept server writes via subscribe responses. Push them inside the `value` object of a `device.{serial}` bucket. | Field | Type | Description | | ------------------------------- | ------- | -------------------------------------------------------------------- | | `away_temperature_high` | float | Upper eco temperature (°C) | | `away_temperature_high_enabled` | boolean | Enable upper eco limit | | `away_temperature_low` | float | Lower eco temperature (°C) | | `away_temperature_low_enabled` | boolean | Enable lower eco limit | | `temperature_scale` | string | Display unit: `"F"` or `"C"` (display only — data is always Celsius) | | `upper_safety_temp` | float | Upper safety limit (°C) | | `upper_safety_temp_enabled` | boolean | Enable upper safety limit | | `lower_safety_temp` | float | Lower safety limit (°C) | | `lower_safety_temp_enabled` | boolean | Enable lower safety limit | | Field | Type | Description | | --------------------- | ------- | -------------------------------------------------------- | | `fan_mode` | string | `"off"`, `"auto"`, or `"duty-cycle"` | | `fan_timer_duration` | integer | Fan timer length (seconds) | | `fan_timer_timeout` | integer | Fan timer end time (Unix timestamp) — set to `0` to stop | | `fan_duty_cycle` | integer | Minutes per hour for duty-cycle mode | | `fan_cooling_enabled` | boolean | Airwave — runs fan with AC coil residual cold | | `fan_duty_start_time` | integer | Fan schedule start (seconds from midnight) | | `fan_duty_end_time` | integer | Fan schedule end (seconds from midnight) | | `fan_schedule_speed` | string | Fan speed during scheduled runs | | Field | Type | Description | | ---------------------------- | ------- | ---------------------------------------- | | `temperature_lock` | boolean | Enable temperature lock | | `temperature_lock_pin_hash` | string | Lock PIN hash | | `temperature_lock_high_temp` | float | Max allowed temperature when locked (°C) | | `temperature_lock_low_temp` | float | Min allowed temperature when locked (°C) | | Field | Type | Description | | --------------------------------------- | ------- | ------------------------------------------------- | | `learning_mode` | boolean | Auto-schedule learning enabled | | `schedule_learning_reset` | boolean | Reset all learned schedule data | | `preconditioning_enabled` | boolean | Start heating/cooling before schedule transitions | | `max_nighttime_preconditioning_seconds` | integer | Max preconditioning duration at night | | Field | Type | Description | | ------------------------- | ------- | -------------------------- | | `target_humidity` | float | Target humidity (%) | | `target_humidity_enabled` | boolean | Enable humidity targeting | | `auto_dehum_enabled` | boolean | Automatic dehumidification | | `humidifier_type` | string | Humidifier type | | `dehumidifier_type` | string | Dehumidifier type | | Field | Type | Description | | ---------------------------------- | ------- | ------------------------------------------------- | | `dual_fuel_breakpoint` | float | Temperature below which aux heat activates (°C) | | `dual_fuel_selected` | boolean | Dual fuel mode active | | `heat_pump_aux_threshold` | float | Aux heat lockout temperature (°C) | | `heat_pump_aux_threshold_enabled` | boolean | Enable aux heat threshold | | `heat_pump_comp_threshold` | float | Compressor lockout temperature (°C) | | `heat_pump_comp_threshold_enabled` | boolean | Enable compressor threshold | | `heatpump_savings` | string | `"max-savings"`, `"balanced"`, or `"max-comfort"` | | Field | Type | Description | | ----------------------------- | ------- | ---------------------------------------- | | `farsight_screen` | string | What the display shows on standby | | `should_wake_on_approach` | boolean | Wake display when someone approaches | | `click_sound` | boolean | Audible click on dial turn | | `sunlight_correction_enabled` | boolean | Compensate for direct sunlight on sensor | | `radiant_control_enabled` | boolean | Optimize for radiant/underfloor heating | | `auto_away_enable` | boolean | Enable occupancy-based auto-away | | `home_away_input` | boolean | Enable home/away feature globally | | Field | Type | Description | | ----------------------------- | ------- | --------------------------------- | | `hot_water_mode` | string | `"schedule"` or `"off"` | | `hot_water_boost_time_to_end` | integer | Boost timer end (Unix timestamp) | | `hot_water_away_enabled` | boolean | Allow hot water during eco mode | | `hot_water_temperature` | float | Hot water target temperature (°C) | *** ## Safety Fields Behavior When any safety-related field changes, the device forces four additional fields into its next PUT regardless of whether they changed: `battery_level`, `safety_temp_activating_hvac`, `safety_state`, `safety_state_time` Accept these extra fields in your normal merge process. # schedule bucket Source: https://docs.nolongerevil.com/nest-protocol/bucket-schedule Temperature schedule — JSON format, sync guards, and how to push/read schedules ## Overview The `schedule` bucket holds the weekly temperature schedule. Unlike most buckets, the device subscribes to this bucket explicitly (it appears in the device's subscribe request body), so you don't need to inject it. **Object key:** `schedule.{serial}`\ **Direction:** Bidirectional (with sync guards)\ **Revision type:** `base_object_revision` (unconditional) *** ## Schedule JSON Format The schedule is a single JSON object with the full weekly schedule. Version is always `2`. ```json theme={null} { "ver": 2, "name": "My Schedule", "schedule_mode": "HEAT", "days": { "0": { "0": {"type": "HEAT", "time": 21600, "entry_type": "setpoint", "temp": 20.0}, "1": {"type": "HEAT", "time": 28800, "entry_type": "setpoint", "temp": 21.5}, "2": {"type": "HEAT", "time": 64800, "entry_type": "setpoint", "temp": 19.0} }, "1": { "0": {"type": "HEAT", "time": 21600, "entry_type": "setpoint", "temp": 20.0} }, "2": {}, "3": {}, "4": {}, "5": {}, "6": {} } } ``` ### Top-Level Fields | Field | Type | Required | Description | | --------------- | ------- | -------- | ---------------------------------------- | | `ver` | integer | Yes | Always `2` | | `name` | string | Yes | Schedule name (e.g., `"Default"`) | | `schedule_mode` | string | Yes | `"HEAT"`, `"COOL"`, or `"RANGE"` | | `days` | object | Yes | Day containers keyed `"0"` through `"6"` | *** ## Day Indexing Days are integer strings starting from **Monday = 0**: | Key | Day | | ----- | --------- | | `"0"` | Monday | | `"1"` | Tuesday | | `"2"` | Wednesday | | `"3"` | Thursday | | `"4"` | Friday | | `"5"` | Saturday | | `"6"` | Sunday | **Monday = 0, not Sunday.** This differs from many programming language conventions (JavaScript `Date.getDay()` uses Sunday = 0). Double-check your day mapping when generating schedules. Within each day, setpoints are keyed by sequential integer strings: `"0"`, `"1"`, `"2"`, etc. *** ## Setpoint Fields | Field | Type | Modes | Description | | ------------ | ------- | ---------- | -------------------------------------------------------------------------- | | `type` | string | All | `"HEAT"`, `"COOL"`, or `"RANGE"` — must match `schedule_mode` | | `time` | integer | All | Seconds from midnight (0–86399) | | `entry_type` | string | All | `"setpoint"` (user-configured) or `"continuation"` (auto-generated filler) | | `temp` | float | HEAT, COOL | Target temperature in °C | | `temp-min` | float | RANGE | Lower bound in °C | | `temp-max` | float | RANGE | Upper bound in °C | ``` 21600 = 6:00 AM (6 × 3600) 28800 = 8:00 AM (8 × 3600) 64800 = 6:00 PM (18 × 3600) ``` All temperatures are **Celsius floats**. The device converts for display based on `temperature_scale`. *** ## Schedule Modes | Mode | Description | Temperature fields | | ------- | ------------------ | ---------------------- | | `HEAT` | Heating only | `temp` | | `COOL` | Cooling only | `temp` | | `RANGE` | Dual heat and cool | `temp-min`, `temp-max` | The active schedule mode is stored in the **shared bucket** as `schedule_mode` — not inside the schedule itself. To switch modes, push `schedule_mode` to the shared bucket. The `schedule_mode` inside the schedule JSON and the `schedule_mode` in the shared bucket must match. If they differ, the device ignores the schedule. *** ## Pushing a Schedule Always push the **complete** schedule — all 7 days, all setpoints. There is no mechanism to add or remove individual setpoints. The device replaces the entire schedule on each push. ```json theme={null} { "objects": [ { "object_revision": 100, "object_timestamp": 1707148800000, "object_key": "schedule.09AA01AB12345678", "value": { ...complete schedule JSON... } } ] } ``` *** ## Reading Schedules from the Device The device sends schedule changes via PUT requests: ```json theme={null} { "session": "sess_xyz789", "schedule.09AA01AB12345678": { "object_key": "schedule.09AA01AB12345678", "base_object_revision": 99, "ver": 2, "name": "Default", "schedule_mode": "HEAT", "days": { ... } } } ``` Note: Schedule data is **inline** in the PUT (not nested in `value`). Store the complete schedule value. Return it on subscribe when the server's version is newer. *** ## Sync Guards Three mechanisms protect schedule integrity: | Guard | Description | | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | | **15-second debounce** | Multiple schedule pushes within 15s — only the last one takes effect. Do not retry within this window. | | **Timestamp rejection** | If your push has an older timestamp than the device's current schedule, the device silently discards it. Always use a current `object_timestamp`. | | **Pending local change** | If the user is editing the schedule on-device, incoming server pushes are silently discarded. The device's local version takes priority. | *** ## Learning Mode Warning When learning mode is enabled, the device's auto-schedule system may modify a schedule you pushed based on subsequent dial turns. To prevent this, disable learning before pushing: ```json theme={null} { "object_key": "device.09AA01AB12345678", "value": { "learning_mode": false } } ``` Then push your schedule. Re-enable learning afterward if desired. # shared bucket Source: https://docs.nolongerevil.com/nest-protocol/bucket-shared Active temperature setpoint, HVAC mode, and current conditions ## Overview The `shared` bucket is the primary bucket for thermostat control. It holds the active temperature setpoint, HVAC mode, and current conditions. This is the bucket you read and write most often. **Object key:** `shared.{serial}`\ **Direction:** Bidirectional\ **Revision type:** `if_object_revision` (conditional write — unique among all buckets) *** ## Conditional Writes The `shared` bucket is the **only** bucket that uses conditional writes. When the device sends a PUT with `if_object_revision`, it expects the server to validate that this revision matches the current stored revision before accepting the write. This prevents the device from overwriting a temperature change the server pushed while the device was preparing its own PUT. All other buckets use `base_object_revision` (unconditional). When a conditional write fails, return `200 OK` with the current `{object_revision, object_timestamp, object_key}` so the device can reconcile. **Never include a `value` field in the response.** *** ## Fields | Field | Type | Server | Description | | ------------------------- | ------- | ------------- | --------------------------------------------------------- | | `target_temperature` | float | Read/write | Target temperature in °C (single-setpoint modes) | | `target_temperature_high` | float | Read/write | Upper bound in range mode (°C) | | `target_temperature_low` | float | Read/write | Lower bound in range mode (°C) | | `target_temperature_type` | string | Read/write | HVAC mode: `heat`, `cool`, `range`, `emergency`, or `off` | | `target_change_pending` | boolean | Read/write | Display wake flag — see below | | `schedule_mode` | string | Read/write | Active schedule mode: `HEAT`, `COOL`, or `RANGE` | | `touched_by` | object | Read/write | Last temperature change source — see below | | `name` | string | Read/write | Device display name | | `current_temperature` | float | **Read-only** | Current indoor temperature (°C) | | `auto_away` | integer | **Read-only** | Occupancy: `0` = home, `1` = away | | `can_heat` | boolean | **Read-only** | Heating capability | | `can_cool` | boolean | **Read-only** | Cooling capability | | `hvac_heater_state` | boolean | **Read-only** | Primary heater running | | `hvac_heat_x2_state` | boolean | **Read-only** | Stage 2 heat running | | `hvac_heat_x3_state` | boolean | **Read-only** | Stage 3 heat running | | `hvac_aux_heater_state` | boolean | **Read-only** | Auxiliary heater running | | `hvac_alt_heat_state` | boolean | **Read-only** | Alternate heat source running | | `hvac_emer_heat_state` | boolean | **Read-only** | Emergency heat running | | `hvac_ac_state` | boolean | **Read-only** | Air conditioning running | | `hvac_cool_x2_state` | boolean | **Read-only** | Stage 2 cooling running | | `hvac_cool_x3_state` | boolean | **Read-only** | Stage 3 cooling running | | `hvac_fan_state` | boolean | **Read-only** | Fan running | *** ## target\_change\_pending — Display Wake When you push a temperature change to a sleeping device, the device wakes and applies the new setpoint — but the **physical display stays off** unless you also set `target_change_pending: true`. **Set `target_change_pending: true` when changing:** * `target_temperature` * `target_temperature_high` * `target_temperature_low` **Don't set it when changing:** * `target_temperature_type` (mode changes have separate display handling) * `schedule_mode` * Any field in the `device` or `structure` buckets **Acknowledgment flow:** 1. Server pushes `target_temperature` + `target_change_pending: true` 2. Device wakes, applies temperature, lights the display 3. Device sends PUT with `target_change_pending: false` 4. Server accepts `false` and does **not** push `true` again If you keep pushing `target_change_pending: true` after the device cleared it, the display keeps cycling — never accept the device's `false` and you get an update loop. *** ## touched\_by — Change Source Tracking The `touched_by` object tells the device what caused the last temperature change. The device uses it to show "Holding until..." after a manual dial turn. ```json theme={null} { "touched_by": { "touched_by": 3, "touched_at": 1707148800, "touched_tzo": -18000, "touched_user_id": "" } } ``` | `touched_by` value | When to use | | ------------------ | ------------------------------------------ | | `1` | Server applying a schedule transition | | `2` | Device user turned the dial | | `3` | External app or API pushed the temperature | Set `touched_at` to the current Unix timestamp in **seconds** (not ms). Set `touched_tzo` to the device's UTC offset in seconds. The device does **not** include `touched_by` in its own temperature PUTs. Your server is responsible for setting this field when pushing temperature changes. *** ## target\_temperature vs Schedule Setpoints `target_temperature` in the shared bucket can be overwritten by: * Schedule transitions (device evaluates schedule locally on its own timer) * Manual dial turns * Cloud pushes from your server **Critical:** If your server pushes a stale `target_temperature` value that differs from what the device currently has, the device treats it as a new manual override — canceling the current schedule-derived setpoint. **Best practice:** Only push `target_temperature` when you have a genuine user-initiated change. Do not echo back the last known value on every subscribe response. *** ## Example: Push Temperature Change ```json theme={null} { "objects": [ { "object_revision": 458, "object_timestamp": 1707149000000, "object_key": "shared.09AA01AB12345678", "value": { "target_temperature": 21.5, "target_change_pending": true, "touched_by": { "touched_by": 3, "touched_at": 1707149000, "touched_tzo": -18000, "touched_user_id": "" } } } ] } ``` # structure bucket Source: https://docs.nolongerevil.com/nest-protocol/bucket-structure Home-level settings — eco mode, away state, and device associations ## Overview The `structure` bucket holds home-level settings that apply to all devices associated with a structure. It is the primary mechanism for eco (away) mode control. **Object key:** `structure.{structureId}`\ **Direction:** Server → device only\ **The device never sends this bucket in PUT requests.** *** ## Fields | Field | Type | Description | | ---------------------- | ------- | ------------------------------------------------------------------------------------- | | `manual_eco_all` | boolean | Eco mode control — `true` activates eco immediately | | `manual_eco_timestamp` | integer | Unix timestamp in **seconds** of the eco change. Must be within 600s of device clock. | | `away` | boolean | Delayed eco trigger — device starts a timer and enters eco if uninterrupted | | `name` | string | Structure name | | `devices` | array | JSON array of device serial numbers | | `country_code` | string | ISO country code | | `postal_code` | string | Postal/ZIP code | | `time_zone` | object | Timezone information | | `house_type` | string | House type classification | | `num_thermostats` | string | Number of thermostats in the structure | | `dr_reminder_enabled` | boolean | Demand response reminders enabled | *** ## Eco Mode ### Enter Eco Mode Set `manual_eco_all: true` with a current `manual_eco_timestamp`: ```json theme={null} { "objects": [ { "object_revision": 5, "object_timestamp": 1707148800000, "object_key": "structure.default", "value": { "manual_eco_all": true, "manual_eco_timestamp": 1707148800 } } ] } ``` `manual_eco_timestamp` is in **Unix seconds** (not milliseconds). The device silently rejects the eco change if this value differs from the device's clock by more than 600 seconds. Keep your server clock NTP-synchronized. Eco activates immediately when the device processes the update. The device switches to eco temperature bounds (`away_temperature_high`/`away_temperature_low` in the device bucket) and suspends schedule-based HVAC control. ### Exit Eco Mode Exiting eco requires sending three things together: ```json theme={null} { "objects": [ { "object_revision": 6, "object_timestamp": 1707148801000, "object_key": "structure.default", "value": { "manual_eco_all": false, "manual_eco_timestamp": 1707148801, "away": false } }, { "object_revision": 7, "object_timestamp": 1707148801000, "object_key": "device.09AA01AB12345678", "value": { "eco": { "mode": "schedule", "touched_by": 3, "mode_update_timestamp": 1707148801 } } } ] } ``` Why all three: * `manual_eco_all: false` — clears eco (but subject to timestamp validation) * `away: false` — alternate exit path (not validated by timestamp) * `eco.mode: "schedule"` in device bucket — **most reliable exit** — no timestamp validation, applied unconditionally After exit, the device performs a fresh schedule lookup — any previous manual temperature override is not restored. ### `away` vs `manual_eco_all` | | `manual_eco_all` | `away` | | ------------------------ | ------------------------------ | -------------------------------- | | Activation | **Immediate** | Delayed (device internal timer) | | Timestamp validation | Yes (600s window) | `true` validated, `false` is not | | Use case | Direct eco toggle (app button) | Occupancy-based automation | | Schedule preconditioning | Blocked | Can end eco early | For a user-facing "Eco" button, use `manual_eco_all`. For home/away automation, use `away`. *** ## Delivering the Structure Bucket The device does **not** subscribe to the structure bucket by default. Your server must actively include it in subscribe responses. Once the device receives a structure bucket, it remembers the structure key and includes it in subsequent subscribe requests. From that point on, structure updates flow through the normal subscribe mechanism. Include the structure bucket on **every subscribe** for paired devices, alongside the user bucket: ```json theme={null} { "objects": [ { "object_revision": 1, "object_timestamp": 1707148800000, "object_key": "user.homeassistant", "value": { "name": "homeassistant" } }, { "object_revision": 1, "object_timestamp": 1707148800000, "object_key": "structure.default", "value": { "name": "Home", "devices": ["09AA01AB12345678"] } } ] } ``` *** ## Choosing the Structure Key | Scenario | Structure key | | -------------------------------- | ----------------------------------------------------------------- | | Device is claimed (has an owner) | Derive from user/owner identity (e.g., `structure.homeassistant`) | | Device is unclaimed | `structure.default` | The device processes the bucket identically regardless of the key name — what matters is that the bucket type is `structure`. # Bucket System Source: https://docs.nolongerevil.com/nest-protocol/bucket-system How device state is organized — object keys, revisions, timestamps, and all 28 bucket types ## Overview Device state is organized into named containers called **buckets**. Each bucket has an `object_key`, `object_revision`, and `object_timestamp`. All communication between device and server is framed in terms of buckets. *** ## Object Key Format ``` {type}.{identifier} ``` Examples: * `device.09AA01AB12345678` — device bucket for serial `09AA01AB12345678` * `shared.09AA01AB12345678` — shared bucket for the same device * `structure.abc123` — structure bucket for a home * `user.homeassistant` — user bucket for pairing The type prefix determines how the device processes the data. The identifier is usually the device serial number but varies by bucket type (e.g., structure IDs, user IDs). *** ## Revision and Timestamp Every bucket tracks two versioning fields: | Field | Type | Purpose | | ------------------ | ----- | ---------------------------------------------------------- | | `object_revision` | int32 | Monotonically increasing write counter | | `object_timestamp` | int64 | Milliseconds since Unix epoch — **primary sync authority** | ### Sync Rules **Timestamp is the sole authority** for determining which data is newer: | Condition | Result | | ----------------------------------------------------- | ------------------------------------------------------------- | | `server_timestamp > device_timestamp` | Accept server data | | `device_timestamp > server_timestamp` | Keep local data (server data is stale) | | Timestamps equal, `server_revision > device_revision` | Accept server data | | Timestamps equal, `server_revision ≤ device_revision` | Keep local data | | `server_timestamp = 0` | Sentinel: server has no data — device should upload its state | ### Zero Timestamp Sentinel A timestamp of `0` signals "no data exists." When the device receives `object_timestamp: 0`, it treats this as an invitation to upload its current local state via PUT. This happens after device reset or server-side data deletion. ### Conditional Writes Most buckets use `base_object_revision` in PUT requests — informational only, no validation. One exception: **The `shared` bucket** uses `if_object_revision` — a conditional write guard. If the revision doesn't match the server's current revision, the server should reject the write. This prevents the device from overwriting a temperature change the server pushed while the device was preparing its PUT. *** ## All 28 Bucket Types | Bucket | Object Key | Direction | Priority | | ----------------------- | --------------------------------- | -------------------------- | ----------- | | `device` | `device.{serial}` | Bidirectional (restricted) | Essential | | `shared` | `shared.{serial}` | Bidirectional | Essential | | `structure` | `structure.{structureId}` | Server → device | Essential | | `user` | `user.{userId}` | Server → device | Essential | | `schedule` | `schedule.{serial}` | Bidirectional (guarded) | Essential | | `where` | `where.{whereId}` | Bidirectional | Secondary | | `message` | `message.{messageId}` | Bidirectional | Secondary | | `link` | `link.{linkId}` | Server → device | Secondary | | `custom_schedule` | `custom_schedule.{id}` | Bidirectional | Secondary | | `device_alert_dialog` | `device_alert_dialog.{id}` | Server → device | Secondary | | `hvac_partner` | `hvac_partner.{partnerId}` | Bidirectional | Specialized | | `topaz` | `topaz.{topazId}` | Server → device | Specialized | | `kryptonite` | `kryptonite.{sensorId}` | Bidirectional | Specialized | | `servicegroup` | `servicegroup.{id}` | Server → device | Specialized | | `occupancy` | `occupancy.{serial}` | Device → server | Specialized | | `demand_response` | `demand_response.{id}` | Bidirectional | Specialized | | `demand_response_event` | `demand_response_event.{eventId}` | Bidirectional | Specialized | | `utility` | `utility.{id}` | Server → device | Specialized | | `diamond_sensor_config` | `diamond_sensor_config.{id}` | Server → device | Specialized | | `diamond_sensor_event` | `diamond_sensor_event.{id}` | Bidirectional | Specialized | | `rate_plan` | `rate_plan.{id}` | Server → device | Specialized | | `tou` | `tou.{id}` | Bidirectional | Specialized | | `demand_charge` | `demand_charge.{id}` | Server → device | Specialized | | `demand_charge_event` | `demand_charge_event.{eventId}` | Bidirectional | Specialized | | `rcs_settings` | `rcs_settings.{id}` | Bidirectional | Specialized | | `cloud_algo` | `cloud_algo.{id}` | Bidirectional | Specialized | | `diagnostics` | `diagnostics.{id}` | Bidirectional | Specialized | | `tuneups` | `tuneups.{id}` | Bidirectional | Specialized | **Direction key:** | Direction | Meaning | | --------------- | ------------------------------------------------------------ | | Server → device | Server pushes on subscribe; device never sends in PUT | | Device → server | Device sends via PUT; server stores | | Bidirectional | Both sides read and write (some have per-field restrictions) | For most home server implementations, only the **essential** buckets need active handling: `device`, `shared`, `structure`, `user`, and `schedule`. *** ## Write Protection The `device` bucket has 113 device-only fields that the server cannot write. If you push a value for one of these fields, the device compares it against its local value and if different, re-sends its own value in the next PUT — actively overwriting your change. Accept these re-PUTs normally. Do not try to fight them. See [device bucket](/nest-protocol/bucket-device) for the full list. *** ## Merge Strategy * **Subscribe responses (server → device):** Shallow merge — device applies the fields in `value` to its local state * **Schedule bucket:** Full replacement — always push the complete schedule JSON * **PUT requests (device → server):** Inline merge — data fields mixed with metadata at the top level # user bucket Source: https://docs.nolongerevil.com/nest-protocol/bucket-user Pairing completion — the bucket that dismisses the setup screen ## Overview The `user` bucket exists for one purpose: triggering pairing completion on the device. When the device receives a user bucket with a `name` field, it records the value as its pairing token and dismisses the setup screen. **Object key:** `user.{userId}`\ **Direction:** Server → device only\ **The device ignores all fields except `name`.** *** ## Fields | Field | Type | Description | | ------ | ------ | --------------------------------------------------------------- | | `name` | string | User identifier — **triggers pairing completion on the device** | All other fields in this bucket are ignored by the device. *** ## When to Push It Push the user bucket immediately after a user claims the device with an entry code. Push it alongside the structure bucket: ```json theme={null} { "objects": [ { "object_revision": 1, "object_timestamp": 1707148800000, "object_key": "user.homeassistant", "value": { "name": "homeassistant" } }, { "object_revision": 1, "object_timestamp": 1707148800000, "object_key": "structure.default", "value": { "name": "Home", "devices": ["09AA01AB12345678"] } } ] } ``` The `userId` in the object key can be any string. NLE uses `"homeassistant"` by default. The `name` field value inside `value` must match the userId in the object key. *** ## Maintain Across Reconnections Include the user bucket on **every subscribe response** for paired devices, not just at registration time. The device may reboot, or the server may restart and lose in-memory state. When the device already has the latest version of the bucket (matching timestamp), it ignores the duplicate — there is no performance penalty for including it on every subscribe. After a fresh server start or device reboot, reboot the device to force a full state sync. During reboot, the device re-initializes all fields and uploads its complete state. *** ## How Pairing Works ```mermaid theme={null} sequenceDiagram participant U as User participant T as Thermostat participant S as Server T->>S: GET /nest/passphrase S-->>T: {value: "A3XR7M2", expires: 1707234600000} T-->>U: Display "A3X-R7M2" loop Poll ~every 30s T->>S: GET /nest/passphrase/status S-->>T: {status: "pending"} end U->>S: POST /api/register {code: "A3XR7M2"} S->>T: Push user bucket (name: "homeassistant") S->>T: Push structure bucket T->>S: GET /nest/passphrase/status S-->>T: {status: "claimed", claimed: true} Note over T: Setup screen dismisses ``` # Connection Architecture Source: https://docs.nolongerevil.com/nest-protocol/connection-architecture How the thermostat and server communicate — full device lifecycle ## Overview The Nest thermostat initiates all connections. The server never dials out to the device. Instead, the device maintains a persistent long-poll connection and relies on the server to push data on it. *** ## Device Lifecycle (Full Flow) ```mermaid theme={null} sequenceDiagram participant D as Thermostat (port 8000) participant S as Server D->>S: POST /nest/entry (Basic Auth — device serial in user ID) S-->>D: {transport_url, passphrase_url, ping_url, ...} D->>S: GET /nest/passphrase (get entry code to display during pairing) S-->>D: {value: "A3XR7M2", expires: 1707234600000} Note over D: Device displays "A3X-R7M2" on screen D->>S: POST /nest/transport (subscribe — chunked) S-->>D: HTTP 200 (Transfer-Encoding: chunked headers sent immediately) Note over D: Device offloads TCP socket to WiFi hardware, CPU sleeps Note over S: Time passes. A command arrives (e.g., set temperature) S-->>D: {hex_size}\r\n{JSON objects array}\r\n Note over D: WoWLAN wakes CPU (~100-500ms) D->>S: POST /nest/transport/put (device acknowledges state change) S-->>D: {object_revision, object_timestamp, object_key} — no value field S-->>D: 0\r\n\r\n (final chunk — connection close) D->>S: POST /nest/transport (device resubscribes) Note over S,D: Cycle repeats — device holds for ~290s then resubscribes ``` *** ## WoWLAN — How the Device Sleeps The thermostat is battery-powered and aggressively manages CPU sleep. After the server sends chunked response headers, the device: 1. Hands the open TCP socket to its WiFi hardware chipset 2. Puts the main CPU to sleep 3. Relies on the WiFi hardware to monitor the socket for incoming data (WoWLAN — WiFi Wake-on-LAN) 4. When data arrives, the WiFi hardware wakes the CPU in \~100–500ms **From the server's perspective:** the connection appears idle but is very much alive. You can push data at any time and the device will wake. **TCP keep-alive must be disabled server-side.** The device cannot respond to TCP keep-alive probes while its CPU is sleeping. Server-side `SO_KEEPALIVE` will cause the OS to close the connection after a few missed probes. The WiFi hardware keeps the connection alive independently. *** ## Subscribe Cycle Timing | Phase | Duration | Who acts | | ------------------- | -------------- | --------------------------------------------------- | | Headers sent | Immediate | Server sends `Transfer-Encoding: chunked` + headers | | Connection held | 0–290s | Server waits silently for data to push | | Data push | When available | Server sends chunk, device wakes in \~100–500ms | | Batch window | ≤3s | Server may send additional chunks before closing | | Close + resubscribe | \~100–500ms | Server sends `0\r\n\r\n`; device reconnects | The server drives the reconnect cycle by closing the connection. The device's `X-nl-suspend-time-max` timer (default 300s) is a **safety net**, not the primary trigger. *** ## Two-Port Architecture The self-hosted server runs two independent HTTP services: ``` ┌─────────────────────────────────────────────────────────┐ │ NoLongerEvil Server │ │ │ │ Port 8000 — Device Protocol API │ │ ┌────────────────────────────────────────────────────┐ │ │ │ Nest protocol emulation (HTTP/1.1, long-poll) │ │ │ │ /nest/entry /nest/transport /nest/passphrase ... │ │ │ │ Called by: thermostat firmware │ │ │ └────────────────────────────────────────────────────┘ │ │ │ │ Port 8082 — Control API │ │ ┌────────────────────────────────────────────────────┐ │ │ │ Simple REST/JSON control API (no auth) │ │ │ │ /command /status /api/devices /api/events ... │ │ │ │ Called by: you (scripts, integrations, dashboard) │ │ │ └────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────┘ ``` The device talks exclusively to port 8000. You talk exclusively to port 8082. The server coordinates state between both ports internally. *** ## Overlapping Subscriptions When a device wakes early (user turns the dial), it sends a new subscribe request before the previous connection has closed. The server briefly sees two active subscriptions for the same device. The correct handling: 1. Track each subscription with a server-generated ID (not the device's `session` field) 2. Push data to **all** active subscriptions when a change occurs 3. Remove a subscription only when its specific connection closes The device's `session` field is reused across all subscribe requests during normal operation — it is not a unique connection identifier. # Device State Model Source: https://docs.nolongerevil.com/nest-protocol/device-state HVAC modes, temperature fields, read-only sensors, and equipment capabilities ## Overview The thermostat's state isn't a single "mode." It's the combination of four independent dimensions, each controlled through different bucket fields. | Dimension | Controls | Bucket | Key field | Server-writable? | | -------------------- | ------------------------- | ----------- | ----------------------------------- | ------------------ | | HVAC mode | Heating, cooling, or both | `shared` | `target_temperature_type` | Yes | | Temperature setpoint | Target temperature | `shared` | `target_temperature` (and variants) | Yes | | Eco mode | Energy-saving state | `structure` | `manual_eco_all` | Yes | | HVAC operation | What hardware is running | `shared` | `hvac_*_state` fields | **No** (read-only) | These dimensions are independent — changing one doesn't reset the others. *** ## HVAC Modes Set the HVAC mode by pushing `target_temperature_type` in the shared bucket: | Value | Behavior | Temperature fields | Wiring required | | ------------- | ------------------------ | --------------------------------------------------- | ------------------------------------- | | `"heat"` | Heating only | `target_temperature` | `can_heat: true` | | `"cool"` | Cooling only | `target_temperature` | `can_cool: true` | | `"range"` | Auto heat+cool | `target_temperature_low`, `target_temperature_high` | `can_heat` and `can_cool` | | `"off"` | All HVAC disabled | None | — | | `"emergency"` | Emergency/auxiliary heat | `target_temperature` | `has_emer_heat: true` (device bucket) | Values are case-insensitive. The device sends lowercase in PUT requests. **Always validate against device capabilities before pushing a mode.** If you push an unsupported mode, the device silently falls back to a mode its wiring supports. ``` can_heat → check shared bucket can_cool → check shared bucket has_emer_heat → check device bucket ``` *** ## Temperature Fields All temperatures are **Celsius floats**. The device converts for display based on `temperature_scale` in the device bucket. | Active mode | Set these fields | | ----------- | ------------------------------------------------------ | | `heat` | `target_temperature` | | `cool` | `target_temperature` | | `emergency` | `target_temperature` | | `range` | `target_temperature_low` AND `target_temperature_high` | | `off` | None | Valid range: 4.5°C – 32°C (40°F – 90°F). Minimum meaningful increment: \~0.01°C. In range mode, the device heats when the current temperature drops below `target_temperature_low` and cools when it rises above `target_temperature_high`. *** ## Equipment Capabilities Check these before offering controls in a UI: | Field | Bucket | Description | | ----------------------- | ------ | ------------------------------- | | `can_heat` | shared | Heating wiring detected | | `can_cool` | shared | Cooling wiring detected | | `has_emer_heat` | device | Emergency/auxiliary heat wiring | | `has_fan` | device | Fan control wiring | | `has_humidifier` | device | Humidifier wiring | | `has_dehumidifier` | device | Dehumidifier wiring | | `has_hot_water_control` | device | Hot water system (UK/EU) | These are reported by the device via PUT and are read-only on the server side. *** ## HVAC Operation State The device reports what equipment is currently running through boolean fields in the shared bucket: | Currently... | Check these fields | | ----------------- | --------------------------------------------------------------- | | Heating | `hvac_heater_state`, `hvac_heat_x2_state`, `hvac_heat_x3_state` | | Cooling | `hvac_ac_state`, `hvac_cool_x2_state`, `hvac_cool_x3_state` | | Emergency heating | `hvac_emer_heat_state` | | Auxiliary heat | `hvac_aux_heater_state`, `hvac_alt_heat_state` | | Fan running | `hvac_fan_state` | Multiple fields can be `true` simultaneously (e.g., stage 1 and stage 2 heating). *** ## Read-Only Sensor Fields | Field | Bucket | Type | Description | | ------------------------- | ------ | ------- | ------------------------------------------ | | `current_temperature` | shared | float | Indoor temperature (°C) | | `current_humidity` | device | integer | Indoor relative humidity (%) | | `backplate_temperature` | device | float | Backplate sensor temperature (°C) | | `battery_level` | device | float | Battery charge level | | `time_to_target` | device | integer | Estimated seconds until target temperature | | `time_to_target_training` | device | string | `"ready"`, `"training"`, or `"not_ready"` | | `auto_away` | shared | integer | Occupancy: `0` = home, `1` = away | *** ## Eco Mode State The device reports its eco state via the `eco_mode` field in the device bucket (a JSON string): ```json theme={null} {"mode":"manual-eco","touched_by":3,"mode_update_timestamp":1738800000,"touched_user_id":"userId"} ``` | `mode` value | Meaning | | -------------- | --------------------------------------------------------------- | | `"schedule"` | Not in eco mode — following schedule normally | | `"manual-eco"` | Server-initiated eco (via `manual_eco_all` in structure bucket) | | `"auto-eco"` | Device-initiated eco (occupancy sensor or `away` timer) | *** ## Emergency Heat Emergency heat bypasses the heat pump compressor and runs auxiliary heat directly. It's expensive and intended for equipment failure or extreme cold. When emergency heat activates, the device automatically: * Saves and disables learning mode * Saves and disables auto-away * Blocks preconditioning entirely * Restores all saved settings when emergency heat is turned off Do not leave a device in emergency heat long-term. *** ## State Interaction Matrix | Server action | HVAC mode | Temperature source | Eco | HVAC runs? | | --------------------------- | ----------- | ---------------------- | ------------ | --------------------- | | Set mode to `"heat"` | → Heat | Schedule or manual | Unchanged | If below setpoint | | Set mode to `"off"` | → Off | — | Unchanged | No | | Set `manual_eco_all: true` | Unchanged | → Eco temperatures | → Manual-eco | Only outside eco band | | Set `manual_eco_all: false` | Unchanged | → Schedule setpoint | → Schedule | Recalculated | | Push new schedule | Unchanged | Updated at transitions | Unchanged | Recalculated | | Safety threshold crossed | Overridden | Overridden | Overridden | Forced on | | Set emergency mode | → Emergency | Manual setpoint | Unchanged | Emergency heat | # Implementation Notes Source: https://docs.nolongerevil.com/nest-protocol/implementation-notes Critical quirks, timing constraints, and battery behavior for server implementers ## Critical Quirks These are the most common implementation mistakes. Each one can cause silent, hard-to-debug failures. | # | Quirk | Consequence if wrong | | -- | ------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------- | | 1 | **Always include explicit port in server URLs** (e.g., `:8000`) | WoWLAN fails — server push cannot wake sleeping device | | 2 | **`object_revision` and `object_timestamp` must appear before `object_key`** in JSON | Device silently ignores the update or fails to parse it | | 3 | **`expires` in entry key response must be a JSON number**, not a string | Device rejects response — never displays entry code | | 4 | **Never include `value` field in PUT responses** | Device overwrites its own local state with server data | | 5 | **Never provision credentials via 401 responses** | Device enters credential loop — cycles between defaults and assigned credentials indefinitely | | 6 | **Use `manual_eco_all`, not `away`, for direct eco control** | `away` triggers delayed, schedule-preconditioning-interruptible eco instead of immediate | | 7 | **`manual_eco_timestamp` is Unix seconds, not milliseconds** | Device rejects eco change (600s window validation fails) | | 8 | **Always push complete schedules** — no partial setpoint updates | Device replaces entire schedule; partial push wipes unincluded days | | 9 | **Monday = 0, not Sunday** in schedule day keys | Schedule shifted by one day | | 10 | **All temperatures are Celsius** — never Fahrenheit in data | Device displays wrong values; schedule setpoints off by \~32°F | | 11 | **Connection hold time must be shorter than `X-nl-suspend-time-max`** | Device's fallback timer fires first — unpredictable behavior | | 12 | **Don't set `target_change_pending: true` again after device cleared it** | Creates update loop — display keeps cycling | | 13 | **Don't push stale `target_temperature` values on every subscribe** | Overrides schedule-derived setpoints with old cloud values | | 14 | **Don't push device-only fields** (e.g., `current_temperature`) | Device overwrites your value in the next PUT | | 15 | **Check `can_heat`/`can_cool` before pushing HVAC mode** | Device silently falls back to supported mode — confusing UI state | | 16 | **Include user and structure buckets on every subscribe** for paired devices | Device loses pairing state after reboot | | 17 | **Wait ≥15 seconds between schedule pushes** | Second push lands in debounce window and is silently discarded | *** ## Timing Reference | Timer | Duration | Source | Notes | | --------------------------- | --------------------- | ----------------- | -------------------------------------------------------- | | `X-nl-suspend-time-max` | 300s (recommended) | Server-set header | Device safety-net wake timer; must be ≤350s | | Connection hold time | 290s (= suspend - 10) | Server behavior | Must be shorter than `X-nl-suspend-time-max` | | Device closing window | 5s | Device-internal | After receiving body data; resets on each chunk | | Batch window | ≤3s | Server behavior | Time between chunks on same connection | | Eco timestamp window | ±600s | Device-internal | `manual_eco_timestamp` must match device clock ±10 min | | Schedule debounce | 15s | Device-internal | Sliding window; last push wins | | Immediate timeout | 7s | Device-internal | Applies if response lacks `Transfer-Encoding: chunked` | | Idle connection timeout | \~360s | Device-internal | Connection considered dead; device resubscribes silently | | `X-nl-defer-device-window` | 15–30s (recommended) | Server-set header | Delay for device PUT batching | | `X-nl-disable-defer-window` | 60s (after push) | Server-set header | Suppress defer delay temporarily | *** ## Battery Voltage Thresholds | Voltage | Effect | | -------- | --------------------------------------- | | 3.8V+ | Normal operation | | 3.7V | +25 seconds added to sleep duration | | 3.65V | Low battery flag set | | **3.6V** | **WiFi disabled — device goes offline** | | 3.5V | +225 seconds added to sleep duration | When battery drops below 3.6V, the device goes completely offline and reconnects only after voltage recovers above 3.8V. **Recommendation:** Queue updates for offline devices and deliver them on the next subscribe after reconnect. *** ## URL Port Requirement **Always include an explicit port in server URLs.** Even for standard ports. ``` WRONG: http://your-server/nest/transport CORRECT: http://your-server:8000/nest/transport ``` Without an explicit port, the device's URL parser fails to extract the port for TCP keepalive offload (WoWLAN). Result: the server can push data but the device does not wake — it stays asleep until its safety-net timer fires. The NLE server automatically appends the explicit port via `api_origin_with_port` when generating URLs in the entry response. *** ## Entry Key `expires` Field The `expires` field in the passphrase response **must be a JSON number**, not a string: ```json theme={null} // CORRECT {"value": "A3XR7M2", "expires": 1707234600000} // WRONG — device silently rejects {"value": "A3XR7M2", "expires": "1707234600000"} ``` The expiration must also be at least 30 minutes in the future from the device's perspective. *** ## Session ID Behavior The device's `session` field in subscribe requests is **not a unique connection identifier**. The device reuses the same session value across all subscribe requests during its operational lifetime. Do not use `session` to track or de-duplicate subscriptions. Generate your own server-side subscription ID for each connection. *** ## Overlapping Subscriptions When a device wakes early, it may send a new subscribe request before the previous connection closes. Your server may briefly hold two active subscriptions for the same device. This is normal. Push data to **all** active subscriptions for a device. Remove a subscription only when its specific connection closes. Never close a subscription because a newer one arrived. # NLE vs Google Nest Cloud Source: https://docs.nolongerevil.com/nest-protocol/nle-differences How NoLongerEvil differs from the original Google Nest cloud implementation ## Overview NoLongerEvil implements the same Nest Cloud Protocol that Google's servers use — the device firmware doesn't know the difference. However, the server internals are substantially simplified for home use. *** ## Authentication and Pairing | | Google Nest Cloud | NoLongerEvil | | ------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------- | | **User auth** | OAuth 2.0 + JWT tokens | None (no user accounts on the device protocol layer) | | **Device auth** | Full credential provisioning via `X-nl-set-client-credentials` | Accepts all credentials — serial extracted from Basic Auth | | **Pairing flow** | Mobile app ↔ cloud ↔ device OAuth exchange | 7-character alphanumeric entry code | | **Credential management** | Provisioned device-specific credentials | No provisioning — default credentials accepted forever | **Why:** NLE skips credential provisioning entirely to avoid the credential loop bug and because home deployments don't need per-device auth secrets. *** ## Pairing States NLE uses a 3-tier pairing model: | State | Meaning | | ----------- | ----------------------------------------------- | | **Paired** | Device has been claimed with a valid entry code | | **Pending** | Entry key exists but hasn't been claimed yet | | **Unknown** | Device has connected but no entry key exists | Google's cloud uses a full OAuth + mobile app pairing flow that NLE replaces with the simpler entry code UI. *** ## Timestamp Sync | | Google Nest Cloud | NoLongerEvil | | ------------------- | --------------------------------------------------- | ------------------------------------------------- | | **Sync resolution** | Timestamp + revision with complex conflict handling | **Timestamp-only** (simplified) | | **Clock authority** | Cloud-authoritative via `X-nl-service-timestamp` | Server-authoritative via `X-nl-service-timestamp` | NLE's simplified timestamp-only sync is sufficient for single-server deployments. Google's cloud handles eventual consistency across distributed infrastructure. *** ## Features Not Implemented These Google Nest cloud features are not available on the self-hosted server: | Feature | Status | | ---------------------------------------- | --------------- | | Away Assist (AI occupancy prediction) | Not implemented | | EnergyIQ / Seasonal Savings | Not implemented | | Rush Hour Rewards / demand response | Not implemented | | Nest Aware video integration | Not implemented | | Person detection | Not implemented | | Farsight / Rush Hour integration | Not implemented | | Multi-user account management | Not implemented | | Shared device permissions | Not implemented | | Nest Home / Works with Nest integrations | Not implemented | *** ## Persistence | | Google Nest Cloud | NoLongerEvil | | ----------------- | -------------------------------------------- | ----------------------------------- | | **Storage** | Distributed cloud databases (Bigtable, etc.) | SQLite (single file) | | **Consistency** | Eventual consistency across regions | Immediate (single process) | | **Structures** | Multiple structures per account | Single default structure per device | | **User accounts** | Full Google account integration | `homeassistant` user ID by default | *** ## MQTT Integration NLE adds MQTT support that the original Nest cloud does not expose: * Device state changes are published to an MQTT broker * Home Assistant and other MQTT-based systems can receive real-time updates * MQTT is optional and configured separately from the core server *** ## What Stays the Same The device protocol itself is identical. The thermostat firmware: * Connects to the same endpoints with the same request format * Receives the same chunked subscribe responses * Uses the same bucket system and sync protocol * Applies the same field ordering requirement * Follows the same timing constraints From the device's perspective, NLE is indistinguishable from Google's cloud. # Overview Source: https://docs.nolongerevil.com/nest-protocol/overview What the Nest Cloud Protocol is and how NoLongerEvil implements it ## What Is the Nest Cloud Protocol? The Nest thermostat uses a custom HTTP-based long-poll protocol to communicate with cloud servers. Rather than WebSockets or REST, the device maintains a persistent HTTP connection where the server holds the response open and streams updates to the device using chunked transfer encoding. This protocol was reverse-engineered and documented by [Chris Serio (cjserio / @SirHeroic)](https://github.com/cjserio) as the [Nest Cloud Protocol Reference](https://github.com/cjserio/nest-thermostat-protocol-docs). The NoLongerEvil self-hosted server is a Python implementation of this protocol. The documentation in this section is drawn from revision 2.9 of the Nest Cloud Protocol Reference by Chris Serio. All credit for the underlying protocol research belongs to the original author. *** ## Key Concepts | Concept | Description | | ------------------------ | ------------------------------------------------------------------------------------------- | | **Subscribe connection** | A persistent HTTP connection where the device waits for server-pushed updates | | **Server push** | The server wakes a sleeping device by sending data on the open connection | | **Chunked encoding** | Required for server push — allows the server to hold connections open and stream data | | **Buckets** | Named data containers (e.g., `device`, `shared`, `schedule`) that hold thermostat state | | **WoWLAN** | WiFi Wake-on-LAN — the WiFi hardware keeps the TCP socket alive while the device CPU sleeps | *** ## Protocol Characteristics | Characteristic | Value | | ---------------- | ------------------------------------------------------- | | Transport | HTTP/1.1 over TLS | | Encoding | JSON request/response bodies | | Connection model | Device-initiated, server-held open | | Push mechanism | Chunked transfer encoding | | Keep-alive | WiFi hardware maintains TCP connection during CPU sleep | | Long-poll cycle | \~290s hold → close → device resubscribes | *** ## Two Communication Patterns The protocol has two distinct flows: **Subscribe (server → device):** 1. Device POSTs its current bucket state (revisions + timestamps) 2. Server immediately sends `Transfer-Encoding: chunked` headers — device offloads connection to WiFi hardware and sleeps 3. Server holds the connection; when state changes, sends a JSON chunk — device wakes via WoWLAN 4. Server closes connection after sending; device resubscribes **PUT (device → server):** 1. Device POSTs state changes (user dial adjustment, sensor readings) 2. Server stores the data, increments revision 3. Server returns `200 OK` with `{object_revision, object_timestamp, object_key}` — no `value` field *** ## NoLongerEvil Implementation The NLE self-hosted server implements this protocol on **port 8000** (the Device Protocol API). Thermostats connect here automatically after being configured to point at the server. Key implementation differences from Google's Nest cloud: * **3-tier pairing** using 7-character entry codes instead of OAuth/JWT * **SQLite persistence** instead of distributed cloud storage * **No credential provisioning** — the server accepts all credentials and identifies devices by serial from Basic Auth * **Single default structure** per device For the full list of differences, see [NLE Differences](/nest-protocol/nle-differences). *** ## Navigation Full device lifecycle diagram HTTP Basic Auth and entry key pairing Subscribe, push, PUT in depth How device state is organized # Temperature Schedules Source: https://docs.nolongerevil.com/nest-protocol/schedules How to push, read, and manage weekly temperature schedules ## Overview The thermostat maintains a weekly temperature schedule that determines its target temperature throughout the day. The server can push schedules to the device and receive schedule updates when the user modifies them locally. Schedules are stored in the `schedule.{serial}` bucket. See [schedule bucket](/nest-protocol/bucket-schedule) for the sync protocol details. *** ## Complete Schedule Example A full week heating schedule: ```json theme={null} { "ver": 2, "name": "Weekday/Weekend", "schedule_mode": "HEAT", "days": { "0": { "0": {"type": "HEAT", "time": 21600, "entry_type": "setpoint", "temp": 19.5}, "1": {"type": "HEAT", "time": 28800, "entry_type": "setpoint", "temp": 21.0}, "2": {"type": "HEAT", "time": 57600, "entry_type": "setpoint", "temp": 19.0}, "3": {"type": "HEAT", "time": 64800, "entry_type": "setpoint", "temp": 21.0}, "4": {"type": "HEAT", "time": 79200, "entry_type": "setpoint", "temp": 18.5} }, "1": { "0": {"type": "HEAT", "time": 21600, "entry_type": "setpoint", "temp": 19.5}, "1": {"type": "HEAT", "time": 28800, "entry_type": "setpoint", "temp": 21.0}, "2": {"type": "HEAT", "time": 64800, "entry_type": "setpoint", "temp": 21.0}, "3": {"type": "HEAT", "time": 79200, "entry_type": "setpoint", "temp": 18.5} }, "2": { "0": {"type": "HEAT", "time": 21600, "entry_type": "setpoint", "temp": 19.5}, "1": {"type": "HEAT", "time": 28800, "entry_type": "setpoint", "temp": 21.0}, "2": {"type": "HEAT", "time": 64800, "entry_type": "setpoint", "temp": 21.0}, "3": {"type": "HEAT", "time": 79200, "entry_type": "setpoint", "temp": 18.5} }, "3": { "0": {"type": "HEAT", "time": 21600, "entry_type": "setpoint", "temp": 19.5}, "1": {"type": "HEAT", "time": 28800, "entry_type": "setpoint", "temp": 21.0}, "2": {"type": "HEAT", "time": 64800, "entry_type": "setpoint", "temp": 21.0}, "3": {"type": "HEAT", "time": 79200, "entry_type": "setpoint", "temp": 18.5} }, "4": { "0": {"type": "HEAT", "time": 21600, "entry_type": "setpoint", "temp": 19.5}, "1": {"type": "HEAT", "time": 28800, "entry_type": "setpoint", "temp": 21.0}, "2": {"type": "HEAT", "time": 64800, "entry_type": "setpoint", "temp": 21.0}, "3": {"type": "HEAT", "time": 79200, "entry_type": "setpoint", "temp": 18.5} }, "5": { "0": {"type": "HEAT", "time": 25200, "entry_type": "setpoint", "temp": 20.0}, "1": {"type": "HEAT", "time": 36000, "entry_type": "setpoint", "temp": 21.5}, "2": {"type": "HEAT", "time": 79200, "entry_type": "setpoint", "temp": 18.5} }, "6": { "0": {"type": "HEAT", "time": 25200, "entry_type": "setpoint", "temp": 20.0}, "1": {"type": "HEAT", "time": 36000, "entry_type": "setpoint", "temp": 21.5}, "2": {"type": "HEAT", "time": 79200, "entry_type": "setpoint", "temp": 18.5} } } } ``` *** ## Day/Time Encoding Days: `"0"` = Monday ... `"6"` = Sunday\ Time: seconds from midnight | Time value | Clock time | | ---------- | ---------- | | `0` | 12:00 AM | | `21600` | 6:00 AM | | `25200` | 7:00 AM | | `28800` | 8:00 AM | | `57600` | 4:00 PM | | `64800` | 6:00 PM | | `79200` | 10:00 PM | *** ## RANGE Mode Example For a heat-cool system, use `temp-min` and `temp-max` instead of `temp`: ```json theme={null} { "ver": 2, "name": "Range Schedule", "schedule_mode": "RANGE", "days": { "0": { "0": { "type": "RANGE", "time": 25200, "entry_type": "setpoint", "temp-min": 19.0, "temp-max": 24.0 } }, "1": {}, "2": {}, "3": {}, "4": {}, "5": {}, "6": {} } } ``` The device heats when temperature drops below `temp-min` and cools when it rises above `temp-max`. *** ## Pushing a Schedule ```json theme={null} { "objects": [ { "object_revision": 100, "object_timestamp": 1707148800000, "object_key": "schedule.09AA01AB12345678", "value": { ...complete schedule... } } ] } ``` Key rules: * Push the **complete** schedule (all 7 days, all setpoints) * Never push partial updates or individual setpoints * Use `entry_type: "setpoint"` for all setpoints you define * Wait ≥15 seconds between schedule pushes (debounce window) * All temperatures in Celsius *** ## Switching Schedule Mode The active schedule mode is controlled by `schedule_mode` in the **shared** bucket — not the schedule itself: ```json theme={null} { "objects": [ { "object_revision": 459, "object_timestamp": 1707148800000, "object_key": "shared.09AA01AB12345678", "value": { "schedule_mode": "COOL" } } ] } ``` The `schedule_mode` inside the schedule JSON and the `schedule_mode` in the shared bucket must match. If they differ, the device ignores the schedule. *** ## Reading the Device's Schedule The device sends schedule updates via PUT requests. Schedule data is inline in the PUT (not nested in `value`): ```json theme={null} { "session": "sess_xyz789", "schedule.09AA01AB12345678": { "object_key": "schedule.09AA01AB12345678", "base_object_revision": 99, "ver": 2, "name": "Default", "schedule_mode": "HEAT", "days": { ... } } } ``` Store the complete schedule. When the device reconnects, compare timestamps and return the schedule bucket if the server's version is newer. *** ## Edge Cases **Continuation setpoints:** Setpoints with `entry_type: "continuation"` are filler entries auto-generated by the device. You only need `"setpoint"` entries when pushing. When reading from the device, store continuation entries as-is. **Learning mode:** When learning is enabled, the device may modify your pushed schedule over time based on user dial turns. Disable learning (`learning_mode: false` in device bucket) before pushing if you need the schedule to stay exactly as pushed. **Days with no setpoints:** Include every day key in the `days` object, even days with no setpoints (empty object `{}`). This signals the device to use the last setpoint from the previous day as a continuation. # Transport & Long-Poll Source: https://docs.nolongerevil.com/nest-protocol/transport The subscribe/push mechanism — the heart of the Nest protocol ## Overview The transport layer is the core of the Nest protocol. The device maintains a persistent HTTP connection (the "subscribe" connection) and receives server-pushed state updates as chunked responses. Separately, the device sends state changes to the server via PUT requests. *** ## Subscribe Connection ### Request ```http theme={null} POST /nest/transport HTTP/1.1 Host: your-server:8000 Content-Type: application/json Authorization: Basic X-nl-protocol-version: 1 { "chunked": true, "session": "18b430SERIAL", "objects": [ { "object_key": "device.09AA01AB12345678", "object_revision": 42, "object_timestamp": 1707148800000 }, { "object_key": "shared.09AA01AB12345678", "object_revision": 15, "object_timestamp": 1707148800000 }, { "object_key": "schedule.09AA01AB12345678", "object_revision": 3, "object_timestamp": 1707148800000 } ] } ``` The device sends its current bucket revisions and timestamps. The server uses these to decide what to push. The device may also include **inline updates** (local state changes) by adding a `value` field with `object_revision: 0` and `object_timestamp: 0`: ```json theme={null} { "object_key": "shared.09AA01AB12345678", "object_revision": 0, "object_timestamp": 0, "value": { "target_temperature": 22.0 } } ``` ### Response Headers The server **immediately** sends headers before any body: ```http theme={null} HTTP/1.1 200 OK Transfer-Encoding: chunked X-nl-suspend-time-max: 300 X-nl-service-timestamp: 1707148800000 X-nl-defer-device-window: 15 ``` | Header | Required | Description | | ---------------------------- | ----------- | -------------------------------------------------------- | | `Transfer-Encoding: chunked` | Yes | Enables server push and WoWLAN sleep | | `X-nl-suspend-time-max` | Yes | Device safety-net wake timer in seconds (recommend: 300) | | `X-nl-service-timestamp` | Recommended | Server timestamp in ms — used for sync decisions | | `X-nl-defer-device-window` | Recommended | Seconds to delay device PUT after local changes (15–30) | | `X-nl-disable-defer-window` | Optional | Temporarily disables defer delay for N seconds | After sending headers, the device offloads the TCP socket to WiFi hardware and sleeps. The server holds the connection open. ### Response Body (Push) When the server has data to send, it writes a JSON chunk: ``` {hex_size}\r\n {"objects": [...]}\r\n 0\r\n \r\n ``` **JSON field ordering is critical.** The device's JSON parser expects `object_revision` and `object_timestamp` to appear **before** `object_key` in each object. Incorrect ordering causes the device to silently ignore the update. **Correct:** ```json theme={null} {"object_revision": 16, "object_timestamp": 1707149000000, "object_key": "shared.SERIAL", "value": {...}} ``` **Wrong:** ```json theme={null} {"object_key": "shared.SERIAL", "object_revision": 16, "object_timestamp": 1707149000000, "value": {...}} ``` Most JSON libraries don't guarantee field order. Use an ordered dictionary, manual JSON building, or a library that preserves insertion order. ### Push Example ```json theme={null} { "objects": [ { "object_revision": 16, "object_timestamp": 1707149000000, "object_key": "shared.09AA01AB12345678", "value": { "target_temperature": 22.0, "target_change_pending": true } } ] } ``` ### When No Data Is Available If the server has nothing to push, it **holds the connection open silently** — no body, no empty JSON, no `{}`. When the hold time expires (290s), send the final chunk terminator: ``` 0\r\n \r\n ``` This wakes the device and triggers a resubscribe. *** ## Synchronization Logic The server compares timestamps (not revisions) to decide what to push: | Condition | Server action | | ------------------------------------- | ------------------------------------------------- | | `server_timestamp > device_timestamp` | Include bucket in response | | `device_timestamp > server_timestamp` | Do not push (device has newer data) | | `device_timestamp = 0` | Sentinel: device has no data — push current state | **Timestamp is the sole authority for sync decisions.** Revision is used only for conditional writes (see [shared bucket](/nest-protocol/bucket-shared)). *** ## PUT (Device → Server) The device sends state changes via PUT requests. ### Request ```http theme={null} POST /nest/transport/put HTTP/1.1 Host: your-server:8000 Content-Type: application/json X-nl-protocol-version: 1 { "session": "sess_xyz789", "shared.09AA01AB12345678": { "object_key": "shared.09AA01AB12345678", "base_object_revision": 15, "target_temperature": 22.5, "target_temperature_type": "heat" } } ``` PUT requests send bucket data at the top level, keyed by bucket identifier. Data fields are **inline** (not nested in a `value` object). ### Response ```http theme={null} HTTP/1.1 200 OK Content-Type: application/json {"object_revision": 16, "object_timestamp": 1707149000000, "object_key": "shared.09AA01AB12345678"} ``` **Never include a `value` field in a PUT response.** The device treats any `value` field in a PUT response as authoritative cloud data and applies every field in it — the same code path as subscribe, with no filtering. This silently overwrites any local state changes the device made after sending the PUT. Return only `{object_revision, object_timestamp, object_key}`. *** ## Timing Reference | Timer | Duration | Notes | | ----------------------- | ------------------ | ----------------------------------------------------------- | | Connection hold time | 290s (recommended) | Must be shorter than `X-nl-suspend-time-max` | | `X-nl-suspend-time-max` | 300s (recommended) | Device safety-net — must be ≤350s | | Device closing window | 5s | After receiving a chunk; resets on each subsequent chunk | | Batch window | ≤3s | Time between chunks on the same connection | | Eco timestamp window | ±600s | `manual_eco_timestamp` must be within 600s of device clock | | Schedule debounce | 15s | Device waits 15s after receiving a schedule before applying | *** ## Defer Window Headers The defer window controls how quickly the device sends local changes (e.g., dial turns) to the server. **`X-nl-defer-device-window: N`** — device delays sending local changes for up to N seconds. Batches rapid dial adjustments into a single PUT. **`X-nl-disable-defer-window: N`** — temporarily disables the defer delay for N seconds. Use this when you push a temperature change so the device's acknowledgment arrives promptly. ```http theme={null} Transfer-Encoding: chunked X-nl-suspend-time-max: 300 X-nl-service-timestamp: 1707149000000 X-nl-disable-defer-window: 60 ``` *** ## Batching Multiple Pushes If multiple changes arrive quickly (user clicking +/- repeatedly), the server can batch them on a single subscribe connection instead of closing after each chunk: 1. Send the first chunk immediately 2. Hold the connection open for ≤3 seconds 3. Send additional chunks as more data arrives 4. Close after the batch window expires Each chunk must be a complete `{"objects": [...]}` JSON document. The device's 5-second closing timer resets on each chunk received. # Self-Hosted Installation Guide Source: https://docs.nolongerevil.com/self-hosted/installation Complete guide to setting up your own No Longer Evil infrastructure **Before you begin:** * Ensure you have a **Nest Generation 1 or Generation 2** thermostat ([check compatibility](/compatibility)) * This is for **advanced users** comfortable with command line, Docker, and networking * You'll need a **server that can run 24/7** (Raspberry Pi, NAS, VPS, etc.) * **Gen 1 requires device teardown** - only proceed if comfortable with electronics disassembly ## Overview The self-hosted path has two parts: 1. **Set up your NLE server** — pull and run the pre-built Docker image on any always-on machine 2. **Flash your thermostat** — run the same GUI installer as the hosted path, then select "Self-Hosted" in the wizard *** ## Part 1: Set Up Your NLE Server Choose the deployment option that matches your setup: ### Option A — Home Assistant Add-on **Requirements:** * **Home Assistant OS** — does NOT work with Home Assistant Container (bare Docker HA) * **Mosquitto broker add-on** must be installed for MQTT/climate entity discovery **Step-by-step:** 1. **Add the repository to Home Assistant:** Add Repository Or manually: Settings → Add-ons → Add-on Store → three-dot menu → Repositories → paste: ``` https://github.com/codykociemba/NoLongerEvil-HomeAssistant ``` 2. **Find NoLongerEvil HomeAssistant in the Add-on Store and click** Install 3. **⚠️ Before starting: configure `api_origin` first** — Go to the **Configuration** tab of the add-on. Change `api_origin` from the default to your Home Assistant machine's actual LAN IP address (e.g. `http://192.168.1.100:9543` — replace `192.168.1.100` with your HA machine's IP) Home Assistant add-on Configuration tab showing api_origin field **`api_origin` must be set before starting.** The default value (`http://homeassistant.local:9543`) uses mDNS, which Nest devices cannot reliably resolve. If you start the add-on before setting a real IP, the thermostat will fail to connect. **Do not skip this step.** 4. Click Save, then go to the **Info** tab at the top and click Start 5. Enable **Start on boot** and **Watchdog** (auto-restart on crash) 6. (Optional) Enable **Auto update** and **Show in sidebar** for easy dashboard access Your NLE server is now running. Continue to [Part 2](#part-2:-flash-and-configure-via-gui-installer) below to flash your thermostat and connect it to your server. | Port | Purpose | | ---------------------------------- | ------------------------------------------------------------------------ | | `9543` (host) → `8000` (container) | Nest device communication — this is what you enter as the server address | | `8082` | Control API + Web UI (Ingress/sidebar only by default) | You do not need to configure these ports for a standard local setup — the add-on handles this automatically. To expose the dashboard directly (e.g., `http://[HA-IP]:8082`): Settings → Add-ons → No Longer Evil → Configuration → Network → assign a host port to container port `8082`. **Security risk:** Exposing port 8082 directly gives unauthenticated access to the NLE dashboard. If your Home Assistant instance is accessible from outside your network, do not expose this port unless you fully understand the risks and have additional access controls in place. **When the GUI installer runs for an HA add-on user:** * The installer scans the LAN for a running NLE server via the `/info` endpoint * It will detect the add-on at port `9543` * Select **Self-Hosted** in the wizard and confirm the detected address (`http://[HA-IP]:9543`) *** ### Option B — Standalone Docker ```bash theme={null} docker run -d \ -p 8000:8000 \ -p 8082:8082 \ --name nolongerevil-server \ -e API_ORIGIN=http://YOUR_LAN_IP:8000 \ -v nolongerevil-data:/data \ ghcr.io/codykociemba/nolongerevil-selfhosted:latest ``` **`API_ORIGIN` must be your machine's LAN IP** (e.g., `http://192.168.1.50:8000`), not `localhost` or a hostname. The thermostat connects back to this address. The GUI installer reads it from the server's `/info` endpoint during setup. Verify it's running: ```bash theme={null} docker ps | grep nolongerevil curl http://localhost:8082/health ``` Your NLE server is now running. Continue to [Part 2](#part-2:-flash-and-configure-via-gui-installer) below to flash your thermostat. *** ### Option C — Docker Compose ```bash theme={null} git clone https://github.com/codykociemba/NoLongerEvil-SelfHosted.git cd NoLongerEvil-SelfHosted ``` Edit `docker-compose.yml` — set `API_ORIGIN` to your LAN IP, then: ```bash theme={null} docker compose up -d ``` **Full environment variable reference:** | Variable | Required | Default | Description | | ------------------------------ | -------- | --------------------------- | ----------------------------------------------------------------------------------------------------- | | `API_ORIGIN` | ✅ Yes | `http://localhost` | LAN IP:port devices connect to (e.g., `http://192.168.1.50:8000`). **Must be an IP, not a hostname.** | | `SERVER_PORT` | Optional | `8000` | Thermostat device API port (inside container) | | `CONTROL_PORT` | Optional | `8082` | Dashboard/control API port (inside container) | | `REQUIRE_DEVICE_PAIRING` | Optional | `false` | If `true`, require 7-char pairing code before device access | | `ENTRY_KEY_TTL_SECONDS` | Optional | `3600` | Entry key validity in seconds | | `MQTT_HOST` | Optional | (empty) | MQTT broker IP — enables Home Assistant auto-discovery | | `MQTT_PORT` | Optional | `1883` | MQTT broker port | | `MQTT_USER` / `MQTT_PASSWORD` | Optional | (empty) | MQTT credentials | | `MQTT_TOPIC_PREFIX` | Optional | `nolongerevil` | MQTT topic prefix | | `MQTT_DISCOVERY_PREFIX` | Optional | `homeassistant` | HA MQTT discovery prefix | | `DEBUG_LOGGING` | Optional | `false` | Set `true` for verbose logs | | `DEBUG_LOGS_DIR` | Optional | `./data/debug-logs` | Debug log output directory | | `STORE_DEVICE_LOGS` | Optional | (disabled) | Store raw device communication logs to disk | | `SQLITE3_DB_PATH` | Optional | `/app/data/database.sqlite` | SQLite database path | | `CERT_DIR` | Optional | (disabled) | Path to TLS certificates for HTTPS | | `WEATHER_CACHE_TTL_MS` | Optional | `600000` | Weather cache duration in ms | | `MAX_SUBSCRIPTIONS_PER_DEVICE` | Optional | `100` | Max long-poll subscriptions per device | | `SUSPEND_TIME_MAX` | Optional | `600` | Device sleep before fallback wake (seconds) | | `DEFER_DEVICE_WINDOW` | Optional | `15` | Delay before device PUT after local changes (seconds) | | `DISABLE_DEFER_WINDOW` | Optional | `60` | Post-push defer disable window (seconds) | Full reference: [github.com/codykociemba/NoLongerEvil-SelfHosted](https://github.com/codykociemba/NoLongerEvil-SelfHosted) Your NLE server is now running. Continue to [Part 2](#part-2:-flash-and-configure-via-gui-installer) below to flash your thermostat. *** ### Option D — NAS Platforms Most NAS platforms should be supported as long as they can run a Docker container. The steps below cover the most common platforms — if yours isn't listed, refer to Option B (Standalone Docker) as a general guide. > **Image:** `ghcr.io/codykociemba/nolongerevil-selfhosted:latest` 1. Open the **Docker** tab in Unraid UI 2. Click **Add Container** 3. Set **Repository** to: `ghcr.io/codykociemba/nolongerevil-selfhosted:latest` 4. Add Port Mappings: * `8000` → `8000` (API) * `8082` → `8082` (Control) 5. Add Path: Container path `/data` → Host path `/mnt/user/appdata/nolongerevil` 6. Add Variable: `API_ORIGIN` = `http://[UNRAID-IP]:8000` 7. Click **Apply** **Via Apps (TrueNAS SCALE):** 1. Go to **Apps** → **Discover Apps** → search for custom app 2. Use the Docker Compose method via Shell — clone `codykociemba/NoLongerEvil-SelfHosted` and run `docker compose up -d` after setting `API_ORIGIN` **Via Shell:** ```bash theme={null} docker run -d \ -p 8000:8000 \ -p 8082:8082 \ --name nolongerevil-server \ -e API_ORIGIN=http://[TRUENAS-IP]:8000 \ -v /mnt/pool/nolongerevil:/data \ ghcr.io/codykociemba/nolongerevil-selfhosted:latest ``` 1. Open **Container Manager** (formerly Docker app) 2. Go to **Registry** → search `ghcr.io/codykociemba/nolongerevil-selfhosted` 3. Download the image (select `latest`) 4. Go to **Container** → **Create** 5. Configure: * Ports: `8000:8000`, `8082:8082` * Volume: `/docker/nolongerevil` → `/data` * Environment: `API_ORIGIN=http://[SYNOLOGY-IP]:8000` 6. Click **Apply** 1. Go to your Portainer instance → **Containers** → **Add Container** 2. **Image:** `ghcr.io/codykociemba/nolongerevil-selfhosted:latest` 3. **Port mapping:** `8000:8000`, `8082:8082` 4. **Volumes:** bind mount or named volume → `/data` 5. **Env:** `API_ORIGIN=http://[HOST-IP]:8000` 6. **Deploy the container** Your NLE server is now running. Continue to [Part 2](#part-2:-flash-and-configure-via-gui-installer) below to flash your thermostat. *** ### Option E — Python Direct For users who don't have Docker, or developers working on the NLE server codebase. Requires Python 3.11+. ```bash theme={null} git clone https://github.com/codykociemba/NoLongerEvil-SelfHosted.git cd NoLongerEvil-SelfHosted python -m venv .venv source .venv/bin/activate pip install . cp .env.example .env # Edit .env — set API_ORIGIN to your LAN IP (e.g., API_ORIGIN=http://192.168.1.50:8000) nolongerevil-server # Or: python -m nolongerevil.main ``` This runs the same Python server that's packaged inside the Docker image. Use this if Docker isn't available, or if you're contributing to the server codebase and want to run it directly during development. Your NLE server is now running. Continue to [Part 2](#part-2:-flash-and-configure-via-gui-installer) below to flash your thermostat. *** ## Part 2: Flash and Configure via GUI Installer **Platform requirements — read before downloading:** **Windows is not supported and has no workaround.** The Nest thermostat only stays in DFU mode for a few seconds. Windows cannot complete USB driver setup within that window. WSL2 does not help — the USB device passthrough cannot be fast enough. Use macOS or Linux. | Platform | Download | | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | macOS (Intel x64) | [`nolongerevil-macos-x64.zip`](https://github.com/codykociemba/NoLongerEvil-Thermostat/releases/download/v1.0.1/nolongerevil-macos-x64.zip) | | macOS (Apple Silicon M1/M2/M3/M4) | [`nolongerevil-macos-arm64.zip`](https://github.com/codykociemba/NoLongerEvil-Thermostat/releases/download/v1.0.1/nolongerevil-macos-arm64.zip) | | Linux (x64) | [`nolongerevil-linux-appimage.zip`](https://github.com/codykociemba/NoLongerEvil-Thermostat/releases/download/v1.0.1/nolongerevil-linux-appimage.zip) | | Windows | ❌ Not supported | ### Step 1: Enter DFU Mode and Flash Follow the same DFU procedure as the hosted installation — see [Step 3 of the hosted installation guide](/hosted/installation#step-3-enter-dfu-mode) for the full Gen 1 / Gen 2 instructions. Once flashing completes you will see this screen: Installation Complete screen **Firmware flashed successfully** — The bootloader and kernel have been written to the device. Keep the USB cable connected — the device will now begin booting into the new firmware. **If flashing failed mid-way:** Unplug and repeat the back plate sequence, then re-run the installer. A partial flash will not permanently damage the device — the DFU bootloader is always recoverable. *** ### Step 2: Wait for Boot Keep the device plugged in via USB and wait for it to complete its boot sequence — this may take 3–4 minutes. When fully booted, you should see the default Nest temperature screen. **Do not disconnect or power off the device during this time.** *** ### Step 3: Complete the Wizard **Once the device has booted, the installer wizard takes over automatically.** Make sure your NLE server is running before proceeding — the wizard will need to reach it to configure the thermostat. Hosting Mode selection — choose Self-Hosted **Choose hosting mode** — Select **Self-Hosted**. If you haven't started your NLE server yet, go back and complete Part 1 first before continuing. The installer scans your LAN for a running NLE server. If detected, confirm the address. If not found, enter it manually: | Deployment | Server address to enter | | ----------------- | ----------------------------------------------------------------- | | Standalone Docker | `http://[YOUR-SERVER-IP]:8000` | | HA Add-on | `http://[YOUR-HA-IP]:9543` | | Python direct | `http://[YOUR-SERVER-IP]:8000` (or whichever port you configured) | Server Configuration — enter your self-hosted server address **Server configuration** — Enter your server address and click **Continue**. If the server was auto-detected on your LAN, it will appear here automatically — just confirm it's correct. **If the server isn't detected:** Make sure the server is running (`docker ps` or check your HA add-on), that your computer is on the same network, and that `API_ORIGIN` is set to a LAN IP (not `localhost` or a hostname). Server Configuration — Home Assistant auto-detected **Home Assistant auto-detected** — If the installer finds a running NLE add-on on your network, it will show it here. Confirm the address matches your HA machine's IP on port `9543`. SSH Access configuration screen **SSH connection** — The wizard connects to the thermostat over SSH to configure it. The device must still be connected via USB and should have finished booting. **If SSH fails:** Make sure the USB cable is still plugged in and the device shows the Nest temperature screen. Unplug and replug the USB cable, wait 30 seconds, and click Retry. If you choose to keep SSH enabled, this screen lets you change the default SSH password from `nolongerevil` to a password of your choice. You can SSH into the device at any time with `ssh root@[THERMOSTAT-IP]`. SSH Password fields for custom credentials Finding Your Nest on the network **Finding your Nest on the network** — The wizard scans your local network to locate the thermostat after it connects to WiFi. The device needs to be on the same network as your server. **If the Nest isn't found:** Check the WiFi credentials on the Nest device itself and confirm it shows as connected. Alternatively, if you know the thermostat's IP address, you can enter it manually and click **Use**. Configuring Device — provisioning in progress **Configuring device** — The wizard is writing your self-hosted server URL to the thermostat. Do not unplug or power off the device during this step. **If provisioning fails:** Verify your server is still running and reachable, then retry. If it fails repeatedly, see the [Troubleshooting guide](/self-hosted/troubleshooting). Setup Complete — verify the Server URL field shows your self-hosted address **Setup complete** — Verify the Server URL shown matches your self-hosted server address. If it shows the No Longer Evil cloud URL instead, the server was not detected correctly — see the [Troubleshooting guide](/self-hosted/troubleshooting). **After the wizard completes — reboot the thermostat:** The thermostat only sends its complete state (temperature, mode, schedule) to the server during a **fresh boot**. After the wizard finishes, reboot the device from the installer or by holding the display for 10 seconds. Without a reboot, the dashboard may show the device as connected but with no data. *** ## Managing Your Server ### Start/Stop Containers **View running containers:** ```bash theme={null} docker ps ``` **Stop the NLE server:** ```bash theme={null} docker stop nolongerevil-server ``` **Restart the NLE server:** ```bash theme={null} docker restart nolongerevil-server ``` ### View Logs **API Server logs:** ```bash theme={null} docker logs nolongerevil-server ``` **Follow logs in real-time:** ```bash theme={null} docker logs -f nolongerevil-server ``` **Last 50 lines:** ```bash theme={null} docker logs nolongerevil-server --tail 50 ``` ### Update to Latest Version ```bash theme={null} docker pull ghcr.io/codykociemba/nolongerevil-selfhosted:latest docker stop nolongerevil-server docker rm nolongerevil-server # Re-run your original docker run command (data volume is preserved) ``` Or with Docker Compose: ```bash theme={null} docker compose pull docker compose up -d ``` ### Home Assistant — Web UI The NLE server includes a built-in web dashboard you can access directly from Home Assistant. Open the **NoLongerEvil HomeAssistant** add-on in Settings → Add-ons, or click **NLE Thermostats** in the sidebar if you enabled the **Show in sidebar** option. Then click Open Web UI. The Web UI lets you: * View all connected thermostats * Add new thermostats or scan for devices on your network * Modify thermostat controls directly — useful if you prefer not to use the Home Assistant interface *** ## Configure Networking (For Remote Access — Optional) **This section is only needed if you want to access your thermostat from outside your local network (over the internet).** For standard home LAN use, skip this section — the thermostat connects to your server directly on the local network. Configure your router to forward these ports to your server: * **Port 8000** - Thermostat Communication API * **Port 8082** - Control API / Dashboard Set up a domain name or Dynamic DNS (DDNS) service to point to your public IP: * Use a service like DuckDNS, No-IP, or your own domain * Configure it to update automatically if your IP changes For secure HTTPS access, set up SSL certificates: * Use Let's Encrypt with certbot * Or use a reverse proxy like nginx with automatic certificate management ```bash theme={null} sudo apt-get install certbot sudo certbot certonly --standalone -d your-domain.com ``` Configure the `CERT_DIR` environment variable to point to your certificate directory. *** ## Next Steps Solutions to common self-hosting issues Learn about the API endpoints # Self-Hosted Overview Source: https://docs.nolongerevil.com/self-hosted/overview Run your own No Longer Evil infrastructure for complete control ## What is Self-Hosting? Self-hosting means running your own No Longer Evil infrastructure on your own server. Your thermostat connects to **your server**, not the hosted service at nolongerevil.com. **Advanced Users Only**: Self-hosting requires technical troubleshooting skills with command line, Docker, and networking. If you're not comfortable with these, we recommend using the [hosted service](/hosted/overview) instead. ## How It Works Pull and run the pre-built Docker image (`ghcr.io/codykociemba/nolongerevil-selfhosted:latest`) on any always-on machine — or install the Home Assistant add-on if you're on HAOS. Run the GUI installer on macOS or Linux to flash custom firmware to your Nest Gen 1 or Gen 2 thermostat via USB. The firmware redirects all thermostat traffic from Google's servers to **your server**. When the installer wizard asks for a hosting mode, choose **Self-Hosted** and enter your server address. The wizard handles SSH configuration and device registration automatically. Once configured, your thermostat connects to your self-hosted NLE server instead of Google's servers. All communication stays within your infrastructure. **Key Difference from Hosted**: You're responsible for running the NLE server. The hosted service runs this for you. ## What You'll Need Raspberry Pi, NAS, VPS, home server, or cloud VM that can run 24/7 Compatible thermostat [Check compatibility →](/compatibility) Command line, Docker, and basic networking Linux or macOS (Windows not supported) *** ## Components You'll Run When self-hosting, you're responsible for running and maintaining: Handles thermostat device communication Pre-built Docker image: `ghcr.io/codykociemba/nolongerevil-selfhosted:latest` Web interface for device management Publishes climate entities to Home Assistant via MQTT discovery Not sure if self-hosting is right for you? See [Choose Your Path →](/choose-your-path) *** ## Ready to Get Started? **Review requirements** before starting installation **Step-by-step instructions** to set up your self-hosted infrastructure *** ## Need Help? Ask the self-hosting community Report bugs or request features # Self-Hosted Prerequisites Source: https://docs.nolongerevil.com/self-hosted/prerequisites Everything you need before starting self-hosted installation **Self-hosting is for advanced users.** Review all requirements carefully before proceeding. If this seems overwhelming, consider using the [hosted service](/hosted/overview) instead. ## Hardware Requirements ### Server Hardware You need a machine that can run 24/7 to host the No Longer Evil infrastructure: **Minimum Specs:** * **CPU**: 2 cores (x64 or ARM64) * **RAM**: 2GB minimum, 4GB recommended * **Storage**: 10GB free space minimum * **Network**: Ethernet or WiFi (Ethernet preferred) **Recommended Options:** * Raspberry Pi 4 (4GB+ RAM) * NAS device (Synology, QNAP) * VPS/Cloud server (DigitalOcean, AWS, Linode) * Home server or repurposed PC *** ## Software Requirements ### On Your Server **Required** — Runs the NLE server container (`ghcr.io/codykociemba/nolongerevil-selfhosted:latest`) * Linux: `sudo apt-get install docker.io` * macOS: Install Docker Desktop Check: `docker --version` ### On Your Flashing Computer The flashing computer is the macOS or Linux machine you use to run the GUI installer and flash the thermostat. This is often a different machine from your server. * **OS**: Linux or macOS (Windows/WSL NOT supported) * **USB Port**: For connecting thermostat * **USB Cable**: Mini USB (Gen 1) or Micro USB (Gen 2), data-capable **Required on your flashing computer if running Linux** — needed by the GUI installer to communicate with the thermostat over USB. ```bash theme={null} sudo apt-get install build-essential libusb-1.0-0-dev libudev-dev gcc pkg-config ``` macOS: libusb is bundled with the installer — no separate installation needed. **Only needed if building the GUI installer from source** — for developers contributing to the thermostat installer codebase or if the pre-built binary doesn't work on your system. * Version 18 or higher * Check: `node --version` *** ## Technical Skills You should be comfortable with: * **Command line** navigation and commands * **Docker** basics (containers, logs, troubleshooting) * **Networking** (local IPs, port forwarding if needed) * **Linux/Unix** basics (permissions, services, logs) *** ## Network Setup ### Required * Stable local network * Static local IP or DHCP reservation for server (so the thermostat can always reach it) ### For Remote Access (Optional) * Public IP or DDNS service * Port forwarding capability * SSL certificates (Let's Encrypt recommended) *** ## Ready to Install? Start the installation process # Self-Hosted Troubleshooting Source: https://docs.nolongerevil.com/self-hosted/troubleshooting Solutions to common issues when self-hosting No Longer Evil ## Installation Issues **Symptoms**: `docker ps` shows no running containers **Solutions**: 1. **Check Docker is running**: ```bash theme={null} sudo systemctl status docker # Linux # or check Docker Desktop is running on macOS ``` 2. **View container logs**: ```bash theme={null} docker logs nolongerevil-server ``` 3. **Check for port conflicts**: ```bash theme={null} sudo lsof -i :8000 # Check if port 8000 is in use sudo lsof -i :8082 # Check if port 8082 is in use ``` 4. **Restart Docker**: ```bash theme={null} sudo systemctl restart docker # Linux # or restart Docker Desktop on macOS ``` **Ubuntu/Debian**: ```bash theme={null} sudo apt-get update sudo apt-get install --fix-missing build-essential libusb-1.0-0-dev gcc pkg-config ``` **macOS**: ```bash theme={null} # Make sure Homebrew is up to date brew update brew doctor brew install libusb pkg-config ``` *** ## Thermostat Flashing Issues **Try these steps in order**: 1. **Back plate sequence first** — Attach to wall mount, wait 2–3 seconds, then remove. The back plate contact arms DFU mode. 2. **Connect USB immediately after removing from back plate** — Use a data-capable cable (not charge-only) 3. **Charge battery** — Ensure >50% battery 4. **Reboot** — Hold display for 10-15 seconds See [DFU Mode Troubleshooting](/hosted/troubleshooting#dfu-mode-issues) for more details **Common issues**: * Contacts not held long enough - Keep bridging until USB connects * Wrong contacts bridged - Double-check the image in the guide * Battery disconnected - Device won't boot without battery * Screen still on - Make sure screen is off before bridging **Solution**: Try multiple times, ensuring: * Battery is connected * Screen is off * Installer is waiting * Bridge contacts, THEN connect USB while holding bridge **Linux**: 1. **Check USB permissions** — run the installer app with sudo: ```bash theme={null} sudo ./NoLongerEvil-*.AppImage ``` 2. **Add udev rules** for a permanent fix: ```bash theme={null} echo 'SUBSYSTEM=="usb", ATTR{idVendor}=="0451", MODE="0666"' | sudo tee /etc/udev/rules.d/99-omap.rules sudo udevadm control --reload-rules ``` **macOS**: * Grant USB permissions in System Preferences → Security & Privacy **Both**: * Try different USB cable (must be data-capable) * Try different USB port **Back plate sequence reminder**: Attach to back plate (2–3 sec) → remove → plug USB. This must be repeated for each flash attempt. If you plugged USB without the back plate step, the device booted normally instead of entering DFU mode. *** ## Server Connection Issues The NLE server is a standalone service, separate from the firmware flashing tool. It runs as a Docker container using the image `ghcr.io/codykociemba/nolongerevil-selfhosted:latest`, as a Python service, or as a Home Assistant add-on. See the [self-hosted installation guide](/self-hosted/installation) for server setup instructions. **Home Assistant Add-on users:** The add-on's external device port is **9543** (not 8000). The thermostat must be configured to reach `http://[HA-IP]:9543`. If your thermostat isn't connecting: verify `api_origin` in the add-on's Configuration tab is set to an IP address, not `homeassistant.local`. The control dashboard (port 8082) is only accessible via the HA sidebar (Ingress) by default. To expose it directly, go to Settings → Add-ons → No Longer Evil → Configuration → Network and assign a host port to container port 8082. **Check**: 1. **API server is running**: ```bash theme={null} docker ps | grep nolongerevil curl http://localhost:8082/health # Should respond ``` 2. **Firewall allows connections**: ```bash theme={null} sudo ufw allow 8000 # Linux sudo ufw allow 8082 ``` 3. **Port forwarding configured** (if remote access): * Check router settings * Verify ports 8000 and 8082 are forwarded to your server 4. **DNS resolves correctly** (if using domain): ```bash theme={null} nslookup your-domain.com ``` 5. **Thermostat can reach server**: * Check WiFi connection on thermostat * Verify thermostat is on same network (for local-only setup) **Symptoms**: Thermostat shows No Longer Evil firmware but connects to cloud, or shows "No Server" **Method 1 — NLEAPI (easier, web-based):** The thermostat exposes a configuration API at port 8080. Navigate to: ``` http://[THERMOSTAT-IP]:8080/cgi-bin/api/settings ``` Find `cloudregisterurl` in the settings and update it to your server's `/entry` endpoint: * Standalone Docker: `http://[SERVER-IP]:8000/entry` * HA Add-on: `http://[HA-IP]:9543/entry` You can find the exact URL by querying your server's info endpoint: ``` http://[SERVER-IP]:[PORT]/info ``` > **Password protection:** If the thermostat has been powered on for more than 30 minutes, > the NLEAPI is protected by a password. To find the password, SSH into the thermostat and > read `/etc/nestlabs/apikey.txt`: > > ```bash theme={null} > ssh root@[THERMOSTAT-IP] # default password: nolongerevil > cat /etc/nestlabs/apikey.txt > ``` > > Use this value as the password when the NLEAPI prompts for authentication. After saving, reboot the thermostat. *** **Method 2 — SSH / direct file edit (fallback):** ```bash theme={null} ssh root@[THERMOSTAT-IP] # Default password: nolongerevil vi /etc/nestlabs/client.config ``` Find the `cloudregisterurl` entry and update the value to your server's `/entry` endpoint (same values as Method 1 above). Save with `:wq`, then reboot: ```bash theme={null} reboot ``` After reboot: Settings (gear icon) → Nest App → Get Entry Key (only needed if `REQUIRE_DEVICE_PAIRING=true`). **Self-signed certificates**: If using self-signed certs, browsers will warn. You can: * Use Let's Encrypt for free valid certificates * Accept the browser warning (not recommended for production) **Let's Encrypt setup**: ```bash theme={null} sudo apt-get install certbot sudo certbot certonly --standalone -d your-domain.com ``` Then configure `CERT_DIR` in your Docker environment to point to the certificates in `/etc/letsencrypt/live/your-domain.com/` *** ## Docker Issues **View logs to see why**: ```bash theme={null} docker logs nolongerevil-server ``` **Common causes**: * Port already in use * Missing environment variables * Insufficient resources (RAM/CPU) * Database connection failed **Check resource usage**: ```bash theme={null} docker stats ``` **Check port mappings**: ```bash theme={null} docker ps ``` Look for `0.0.0.0:8000->8000/tcp` and `0.0.0.0:8082->8082/tcp` **If ports aren't mapped**, recreate containers with proper port mapping: ```bash theme={null} docker run -p 8000:8000 -p 8082:8082 ... ``` **Check Docker disk usage**: ```bash theme={null} docker system df ``` **Clean up unused resources**: ```bash theme={null} docker system prune -a docker volume prune ``` **Warning**: This will remove all stopped containers and unused images *** ## Network & Remote Access **Check these in order**: 1. **Port forwarding configured**: * Log into your router * Forward ports 8000, 8082 to your server's local IP * Verify forwards are active 2. **Firewall allows traffic**: ```bash theme={null} sudo ufw status sudo ufw allow 8000 sudo ufw allow 8082 ``` 3. **Public IP correct**: ```bash theme={null} curl ifconfig.me # Shows your public IP ``` 4. **Dynamic DNS working** (if using DDNS): * Verify DNS record points to current IP * Check DDNS client is running and updating 5. **ISP not blocking ports**: * Some ISPs block common ports * Try alternative ports or contact ISP **Check certificate**: ```bash theme={null} sudo certbot certificates ``` **Renew if expired**: ```bash theme={null} sudo certbot renew ``` **Test HTTPS endpoint**: ```bash theme={null} curl -k https://your-domain.com # -k skips cert validation ``` **Common issues**: * Certificate not installed correctly * Server not configured for HTTPS (`CERT_DIR` env var not set) * Port 8000 not open * Domain doesn't match certificate *** ## Performance Issues **Check resource usage**: ```bash theme={null} top docker stats df -h # Check disk space ``` **Solutions**: * Allocate more resources to Docker * Enable `DEBUG_LOGGING=false` to reduce log overhead * Use faster storage (SSD vs HDD) **Check which container is using memory**: ```bash theme={null} docker stats ``` **Restart container to free memory**: ```bash theme={null} docker restart nolongerevil-server ``` *** ## Still Having Issues? Ask the self-hosting community for help Report bugs or check existing issues Always check Docker logs first: ``` docker logs nolongerevil-server ``` Consider using the hosted service if self-hosting is too complex # Frequently Asked Questions Source: https://docs.nolongerevil.com/support/faq Common questions about No Longer Evil ## General Questions **Yes.** You own your thermostat and have the right to modify its software under right-to-repair principles. **Possibly.** Flashing custom firmware may void Google/Nest's warranty. However, if your device is out of warranty or Google has discontinued support, this doesn't matter. **Yes.** You can reflash the original Nest firmware to restore factory functionality. Note: We don't provide Google's firmware. You'd need to obtain it separately or contact Nest support. **Generation 1 and Generation 2** are fully supported. * **Gen 2**: Simple USB connection (15-30 minutes) * **Gen 1**: Requires device teardown and contact bridging (30-45 minutes) * **Gen 3**: Not yet supported - under development Join our [Discord](https://discord.gg/AffeGaJ5XS) for Gen 3 updates. **Linux (recommended) or macOS only.** **Windows is NOT supported**, including WSL (Windows Subsystem for Linux). The firmware flashing tools require native Linux or macOS USB drivers that don't work properly in Windows environments. **Yes.** All communication uses SSL/TLS encryption. For the hosted service, we don't sell or share your data. For complete data ownership, consider [self-hosting](/self-hosted/overview). ## Technical Questions **Hosted service users**: Your thermostat will lose remote access but continue working locally. **Self-hosted users**: No impact - you're completely independent. We're committed to open-sourcing everything so the community can continue the project indefinitely. **With self-hosting**: Yes! Run the API server on your local network and your thermostat works without internet. **With hosted service**: No. The thermostat needs internet to reach No Longer Evil servers. **Worst case**: Device might be bricked (unresponsive). **Recovery options**: * Try reflashing (often works) * Contact community for help **Prevention**: Ensure battery is charged, use good USB cable, don't disconnect during flashing. ## Troubleshooting FAQs **Check**: 1. Device is connected to WiFi (Settings → Network) 2. Internet connection is working 3. Firewall isn't blocking port 443 4. Try different network (phone hotspot) to isolate issue See [Network Troubleshooting](/hosted/troubleshooting#network-issues) **Common causes**: * Code expired (>1 hour old) - generate new one * Typo (check 0/O, 1/I/L confusion) * Code already used - generate new one * Device not online - check WiFi See [Entry Code Troubleshooting](/hosted/troubleshooting#entry-code-issues) **Try these steps in order**: 1. **Boot device on backplate first** - Connect to wall mount and let it fully boot to home screen 2. **Remove from wall** - Once booted, remove from backplate 3. **Start installer** - Run `./install.sh` and wait for "Waiting for device..." 4. **Connect USB** - Use data-capable cable (not charge-only) 5. **Charge battery** - Ensure >50% battery 6. **Reboot** - Hold display for 10-15 seconds See [DFU Mode Troubleshooting](/hosted/troubleshooting#dfu-mode-issues) **Gen 1 requires device disassembly** which is more complex than Gen 2. **What you need**: * Phillips screwdriver or prying tool * Metal tweezers (to bridge contacts) * Patience and steady hands If you're not comfortable with electronics teardown, consider: * Getting a Gen 2 device instead (easier to flash) * Asking for help in our [Discord community](https://discord.gg/AffeGaJ5XS) Full instructions with images: [Gen 1 Installation Guide](/hosted/installation#generation-1-advanced) ## Still Have Questions? Ask the community Browse existing Q\&A # How Google Blocks Older Devices Source: https://docs.nolongerevil.com/why-google-blocks Understanding how Google intentionally blocks Gen 1 & Gen 2 Nest Thermostats ## The Problem Google/Nest is intentionally blocking older Generation 1 and Generation 2 thermostats from accessing their API servers, even though these devices are **fully functional and use the exact same API structure as newer models**.