# Schedule

Retrieve and recompute optimized HVAC schedules via /api/v1/schedule.

The schedule endpoint returns today's temperature plan for one HVAC unit: 48 half-hour intervals in a small fixed shape. It suits a client that drives a single thermostat by setpoint.

For multi-appliance schedules (HVAC + EV charger + battery + water heater in one call), and for the per-slot mode and fan schedules, use [`/api/v1/schedules`](/api-reference/appliances/#get-apiv1schedules). This route never returns `hvac_mode_schedule`, `fan_mode_schedule` or `room_target_temps`.

A plan for today can be replaced during the day, after a settings change and when the API re-plans the rest of the day. Poll [`GET /api/v1/schedules/updated-at`](/api-reference/appliances/#get-apiv1schedulesupdated-at) and fetch again when its value changes.

## GET /api/v1/schedule

**Auth:** Required (Bearer JWT)

### Query parameters

| Param | Required | Notes |
|---|---|---|
| `appliance_id` | Conditional | UUID of the HVAC unit whose schedule to return. **Required (returns `400`) when the account has 2+ HVAC units** — otherwise the endpoint can't tell which one you mean. Single-HVAC accounts may omit it; it resolves to their only HVAC. An id that is not one of the caller's own HVAC units returns the defaults payload below, not an error. |

### Example

```bash
curl "https://api.hungrymachines.io/api/v1/schedule?appliance_id=a1b2c3d4-..." \
  -H "Authorization: Bearer YOUR_TOKEN"
```

### Response (200) — stored plan

```json
{
  "date": "2025-11-18",
  "schedule": {
    "intervals": [0, 1, 2, "...", 47],
    "high_temps": [74.0, 74.0, "...(48 values)"],
    "low_temps": [70.0, 70.0, "...(48 values)"],
    "setpoint_temps": [72.0, 71.5, "...(48 values)"],
    "temp_trajectory": [72.1, 71.6, "...(48 values)"]
  },
  "mode": "cool",
  "estimated_savings_pct": 18.5,
  "model_confidence": null,
  "generated_at": "2025-11-18T04:15:00+00:00",
  "stale": false,
  "source": "optimization"
}
```

### Response fields

Every key below is present in every response. A value that does not apply is `null`, never missing.

| Field | Type | Description |
|---|---|---|
| `date` | string | The date of the plan returned (YYYY-MM-DD); today's date on the defaults path |
| `schedule.intervals` | int[48] | Always `[0, 1, ..., 47]` — 48 half-hour slots |
| `schedule.high_temps` | float[48] | Per-interval upper comfort bound (°F). **Display-only** — the thermostat never sees this value. |
| `schedule.low_temps` | float[48] | Per-interval lower comfort bound (°F). **Display-only.** |
| `schedule.setpoint_temps` | float[48] or null | **The 48-element command to apply to the thermostat** at each 30-min boundary, regardless of HVAC mode. `null` when `source` is `"defaults"` (and on plans stored before the field existed) — fall back to `(high_temps[i] + low_temps[i]) / 2`. |
| `schedule.temp_trajectory` | float[48] or null | Predicted indoor-temperature line for charting. **Display-only.** `null` when `source` is `"defaults"`, and on a warm-up day. |
| `mode` | string | `"cool"` or `"heat"` on a stored plan. On the defaults path it echoes the account's `optimization_mode` preference, so it can also be `"auto"` or `"off"`. |
| `estimated_savings_pct` | float | **Display-only** estimated % savings vs. a thermostat held at the middle of your comfort band. `0.0` on the defaults path. |
| `model_confidence` | float or null | **Display-only**, reserved. Currently always `null`; don't build on it. |
| `generated_at` | string | When the plan was produced (UTC ISO 8601); the time of the request on the defaults path |
| `stale` | boolean | `false` when the plan returned is for today, and always `false` on the defaults path. `true` when there is no plan for today and the most recent stored plan is returned instead. |
| `source` | string | `"optimization"` (a stored plan) or `"defaults"` (no stored plan yet) |

> **Caution**
>
> **Apply `setpoint_temps`, not `high_temps`/`low_temps`.** The plan carries a per-interval target temperature in `setpoint_temps`. The high/low arrays are display values for charts. When `setpoint_temps` is `null` (`source: "defaults"`), fall back to the midpoint: `(high_temps[i] + low_temps[i]) / 2`. On this route the key is always present, with `null` when it does not apply.

> **Caution**
>
> **Mode and fan schedules are not on this route.** `hvac_mode_schedule`, `fan_mode_schedule` and `room_target_temps` come only from [`GET /api/v1/schedules`](/api-reference/appliances/#get-apiv1schedules) and the per-appliance schedule route. A client for a window AC, or for any unit with mode or fan control turned on, should read those instead. In `fan_mode_schedule`, `auto` and `off` mean "do not send a fan command": send one only for `low` or `high`, and only when the unit has that fan option.

### The 48-interval shape

Every schedule contains exactly **48 intervals**, each a 30-minute window:

| Interval | Time |
|---|---|
| 0 | 00:00–00:30 |
| 1 | 00:30–01:00 |
| ... | ... |
| 16 | 08:00–08:30 |
| 34 | 17:00–17:30 |
| 47 | 23:30–00:00 |

Slot 0 begins at **local midnight** (the account's configured timezone). Compute the current interval as `interval = hour * 2 + (1 if minute >= 30 else 0)` using local time.

### Response variants

#### Stale schedule

If there is no plan for today, the most recent stored one is returned with `stale: true`, and `date` tells you which day it was made for:

```json
{ "date": "2025-11-17", "stale": true, "source": "optimization", "...": "..." }
```

#### Defaults (no stored plan yet)

Until the first plan is stored for the unit, the response carries a comfort band built from the account's preferences, with `setpoint_temps` and `temp_trajectory` set to `null`:

```json
{
  "date": "2025-11-18",
  "schedule": {
    "intervals": [0, 1, "...", 47],
    "high_temps": [73.5, 73.5, "...(all same)"],
    "low_temps": [70.5, 70.5, "...(all same)"],
    "temp_trajectory": null,
    "setpoint_temps": null
  },
  "mode": "cool",
  "estimated_savings_pct": 0.0,
  "model_confidence": null,
  "generated_at": "2025-11-18T12:00:00+00:00",
  "stale": false,
  "source": "defaults"
}
```

> **Note**
>
> `source` is `"defaults"` only until the first plan is stored for the unit. That happens on the first recompute (a settings save, or `POST /api/v1/schedule/recompute`) or the next overnight run, whichever comes first. From then on `source` is `"optimization"` and `setpoint_temps` is populated every day. Savings can still read `0.0` on early plans, for example before any readings have arrived. Your client doesn't need to detect any later transition; it applies whatever the current plan says.

### Integration pattern

1. Call `GET /api/v1/schedule` when your client starts and after the overnight run.
2. Cache the response locally.
3. Poll [`GET /api/v1/schedules/updated-at`](/api-reference/appliances/#get-apiv1schedulesupdated-at) every few minutes and repeat step 1 when its value changes.
4. Every 30 minutes, look up the current interval `idx` and apply `setpoint_temps[idx]`. If `setpoint_temps` is `null`, apply the midpoint of `high_temps[idx]` and `low_temps[idx]`.

### Errors

| Status | Detail | Cause |
|---|---|---|
| 400 | `"User has multiple HVAC appliances; pass appliance_id to disambiguate."` | 2+ HVAC units and no `appliance_id` supplied |
| 401 | `"Not authenticated"`, `"Invalid token"` or `"Token expired"` | No `Authorization` header, or a bad or expired token |

---

## POST /api/v1/schedule/recompute

Recompute schedules for the account now, wait for the result, and return it. Built for "user just hit Save on a constraint editor — show the new chart now" flows. The response has exactly three keys: `date`, `appliances` (the same per-appliance entries as [`GET /api/v1/schedules`](/api-reference/appliances/#get-apiv1schedules)) and a `results` array reporting each active appliance's outcome for this run. No request body.

**Auth:** Required (Bearer JWT)

### Example

```bash
curl -X POST https://api.hungrymachines.io/api/v1/schedule/recompute \
  -H "Authorization: Bearer YOUR_TOKEN"
```

### Response (200)

The response does **not** carry the top-level `optimization_enabled`, `integration_health` or `divergence_summary` that `GET /api/v1/schedules` returns. Call that route when you need them.

```json
{
  "date": "2025-11-18",
  "appliances": ["<same entries as GET /api/v1/schedules>"],
  "results": [
    { "appliance_id": "uuid-1", "appliance_type": "hvac", "name": "Office AC", "status": "ok" },
    { "appliance_id": "uuid-2", "appliance_type": "hvac", "name": "Front AC", "status": "failed", "detail": "internal error" }
  ]
}
```

**`results[].status` values:**

| Value | Meaning |
|---|---|
| `ok` | A fresh schedule was stored for this appliance. An `ok` entry may carry extra boolean diagnostic keys; ignore them. |
| `failed` | This appliance was not re-planned; `detail` carries a backend-defined reason string (log or display it, don't parse it). Its previous schedule is still in `appliances`. Tell the user it didn't update. |
| `calibration` | A warm-up day is in effect for this HVAC; constraint changes won't show on its chart until it completes. |
| `forecast` | Solar — nothing to schedule, but a forecast row IS written (generation, self-consumption, surplus, export revenue). |
| `observe` | Dehumidifier — data-collection only. |
| `skipped` | Unknown appliance type. |

`results` is an empty array, and `appliances` holds the previous entries, when the run could not start for the whole account: no profile yet, no active appliances, the account's `optimization_mode` is `"off"`, the weather forecast could not be loaded, or the service is temporarily unable to plan.

An appliance that still has no stored plan appears in `appliances` with `"schedule": {}` and `"source": "defaults"`, exactly as on `GET /api/v1/schedules`.

### Latency

Usually a few seconds; allow for about 20 seconds per HVAC unit before timing out. Show an "Optimizing…" overlay while the request is in flight.

### When to use it

- Right after the user changes constraints, comfort bands, or `optimization_mode` and expects to see the impact immediately.
- After saving a custom rate curve via `PUT /api/v1/rates`.

For the regular fetch, use `GET /api/v1/schedule` or `GET /api/v1/schedules` — they read the most recent stored plan without triggering a recompute.

### Errors

| Status | Detail | Cause |
|---|---|---|
| 401 | `"Not authenticated"`, `"Invalid token"` or `"Token expired"` | No `Authorization` header, or a bad or expired token |

Canonical page: https://docs.hungrymachines.io/api-reference/schedule/
