# Preferences

Manage comfort and optimization preferences via GET/PUT /api/v1/preferences.

The preferences endpoints let you configure your comfort settings and optimization mode. These values control how aggressively the optimizer shifts energy usage.

> These are **account-level** preferences. For a home with more than one HVAC, each unit also has its own [per-HVAC preferences](/api-reference/appliances/#per-hvac-preferences) with the same shape. Planning for a unit follows these account-level values until that unit's own preferences are saved, and the account-level values stay authoritative for account-scope fields.

> See also [Rates](/api-reference/rates/) for the electricity rate curve the optimizer saves against.

## GET /api/v1/preferences

**Auth:** Required (Bearer JWT)

### Example

```bash
curl https://api.hungrymachines.io/api/v1/preferences \
  -H "Authorization: Bearer YOUR_TOKEN"
```

### Response (200)

```json
{
  "base_temperature": 72.0,
  "savings_level": 1,
  "time_away": "08:00",
  "time_home": "17:00",
  "optimization_mode": "cool",
  "hourly_high_temps_f": null,
  "hourly_low_temps_f": null,
  "optimize_hvac_fan": false,
  "optimize_hvac_mode": false,
  "optimization_enabled": true,
  "newsletter_opt_in": true,
  "spike_guard_enabled": true,
  "max_concurrent_appliances": null,
  "power_cap_kw": null,
  "base_load_reserve_kw": 0.0,
  "demand_charge_usd_per_kw": null,
  "demand_window_start": null,
  "demand_window_end": null
}
```

If the user hasn't set preferences yet, defaults are returned.

### Fields

| Field | Type | Default | Description |
|---|---|---|---|
| `base_temperature` | float | 72.0 | Desired temperature (°F) |
| `savings_level` | integer | 1 | 1 = tight, 2 = moderate, 3 = aggressive. Stored and returned; it does not change the default comfort band. To widen or narrow comfort, set the two hourly arrays. |
| `time_away` | string | `"08:00"` | When you typically leave home (HH:MM, 24-hour). Stored and returned; it does not change the default comfort band. |
| `time_home` | string | `"17:00"` | When you typically return home (HH:MM, 24-hour). Stored and returned; it does not change the default comfort band. |
| `optimization_mode` | string | `"cool"` | `"cool"`, `"heat"`, `"auto"`, or `"off"` |
| `hourly_high_temps_f` | array[float] \| null | `null` | Optional 24-element per-hour upper comfort bound (°F). When set, the two hourly arrays are the comfort band. When `null`, the default band applies. |
| `hourly_low_temps_f` | array[float] \| null | `null` | Optional 24-element per-hour lower comfort bound (°F). Must be set together with `hourly_high_temps_f`. |
| `optimize_hvac_fan` | boolean | `false` | Opt-in: also emit a per-slot fan-speed schedule. |
| `optimize_hvac_mode` | boolean | `false` | Opt-in: also emit a per-slot HVAC-mode schedule. |
| `optimization_enabled` | boolean | `true` | Master pause switch. When `false`, clients must stop applying schedules entirely until re-enabled. Schedules are still computed, so re-enabling takes effect at the next half-hour boundary. |
| `newsletter_opt_in` | boolean | `true` | Newsletter membership. Changing only this field does not trigger a schedule recompute. |
| `spike_guard_enabled` | boolean | `true` | Automatically avoid real-time price spikes on plans that support it. Your comfort range always applies. Turn off any time. |
| `max_concurrent_appliances` | integer \| null | `null` | Limit how many appliances (EV charger, home battery, water heater, etc.) may run at the same time. `null` = no limit. |
| `power_cap_kw` | float \| null | `null` | A whole-home power ceiling (kW) the planner respects. `null` = no cap. |
| `base_load_reserve_kw` | float | `0.0` | Power (kW) always reserved for everyday always-on draw before the planner spends the rest of `power_cap_kw`. |
| `demand_charge_usd_per_kw` | float \| null | `null` | Your utility's demand charge rate ($/kW), if any, so the planner avoids a new monthly peak. `null` = no demand charge. |
| `demand_window_start` | string \| null | `null` | HH:MM start of the hours your demand charge applies. `null` with `demand_window_end` also `null` = all day. |
| `demand_window_end` | string \| null | `null` | HH:MM end of the hours your demand charge applies. |

### The default comfort band

Without an explicit override, every home gets the same default band, set only by `base_temperature` and `optimization_mode`. At a base of 72 °F in cool mode it is 70–73 °F for hours 0–7 and 22–23, and 67–78 °F for hours 8–21. A different base shifts both bounds by the same amount, and heat mode mirrors the shape so the wider side is below the base. `savings_level`, `time_away` and `time_home` do not change it. See [default bands](#get-apiv1preferencesdefault-bands) below to preview the exact per-hour values.

### Hourly comfort bands (advanced)

If you want different per-hour bounds — for example, a tighter band during occupied hours and a wider band overnight — submit `hourly_high_temps_f` and `hourly_low_temps_f` together.

- Both arrays must be set together, or both must be `null` to clear the override and return to the default band. Always send both in the same request.
- Each array is exactly 24 values, indexed `0` (midnight) through `23` (11 PM local time).
- Each value must be in `[50, 90]` °F.
- For every hour `i`, `hourly_low_temps_f[i] < hourly_high_temps_f[i]`.
- When the override is set, the explicit bounds apply at every hour.

Example: tighter `[68, 74]` band from 8 AM through 9 PM, wider `[64, 78]` band overnight.

```json
{
  "hourly_high_temps_f": [78, 78, 78, 78, 78, 78, 78, 78, 74, 74, 74, 74, 74, 74, 74, 74, 74, 74, 74, 74, 74, 74, 78, 78],
  "hourly_low_temps_f":  [64, 64, 64, 64, 64, 64, 64, 64, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 64, 64]
}
```

---

## PUT /api/v1/preferences

**Auth:** Required (Bearer JWT)

Update one or more preference fields. **All fields are optional** — only the fields you include are changed; others keep their current values. To leave a field unchanged, omit it; send `null` only for the fields the table below marks as clearable. Any change other than `newsletter_opt_in` alone triggers a debounced recompute of today's schedule.

### Example

```bash
curl -X PUT https://api.hungrymachines.io/api/v1/preferences \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "base_temperature": 73.0,
    "savings_level": 2,
    "optimization_mode": "auto"
  }'
```

### Request body

All fields are optional — include only what you want to change.

```json
{
  "base_temperature": 73.0,
  "savings_level": 2,
  "time_away": "07:30",
  "time_home": "18:00",
  "optimization_mode": "auto"
}
```

### Validation

| Field | Constraints |
|---|---|
| `base_temperature` | 60.0–85.0 °F |
| `savings_level` | 1, 2, or 3 |
| `time_away` | HH:MM, 24-hour, `00:00`–`23:59` |
| `time_home` | HH:MM, 24-hour, `00:00`–`23:59` |
| `optimization_mode` | `"cool"`, `"heat"`, `"auto"`, `"off"` |
| `hourly_high_temps_f` | Exactly 24 finite floats, each in `[50, 90]` °F |
| `hourly_low_temps_f` | Exactly 24 finite floats, each in `[50, 90]` °F |
| `hourly_low_temps_f[i]` < `hourly_high_temps_f[i]` | Per-hour lower bound must be strictly less than upper bound |
| `hourly_high_temps_f` + `hourly_low_temps_f` | Both fields must be set together (or both `null` to clear) |
| `optimize_hvac_fan` | `true` / `false` |
| `optimize_hvac_mode` | `true` / `false` |
| `optimization_enabled` | `true` / `false` |
| `newsletter_opt_in` | `true` / `false` |
| `spike_guard_enabled` | `true` / `false` |
| `max_concurrent_appliances` | Integer >= 1, or `null` to clear |
| `power_cap_kw` | Float > 0, or `null` to clear |
| `base_load_reserve_kw` | Float >= 0 |
| `demand_charge_usd_per_kw` | Float >= 0, or `null` to clear |
| `demand_window_start` | HH:MM, 24-hour, `00:00`–`23:59`, or `null` |
| `demand_window_end` | HH:MM, 24-hour, `00:00`–`23:59`, or `null` |

### Response (200)

Returns the full preferences object after the update — every field shown under GET, not only the ones you sent. Abbreviated here:

```json
{
  "base_temperature": 73.0,
  "savings_level": 2,
  "time_away": "07:30",
  "time_home": "18:00",
  "optimization_mode": "auto",
  "...": "the remaining GET fields"
}
```

### Errors

| Status | Detail | Cause |
|---|---|---|
| 400 | `"hourly_high_temps_f and hourly_low_temps_f must be set together or both cleared"` | Only one of the two arrays was provided as a non-null value |
| 400 | `"hourly_high_temps_f and hourly_low_temps_f must each have exactly 24 values"` | Wrong array length |
| 400 | `"hourly bands at hour <i> must be between 50 and 90 F"` | Value out of range at hour `<i>` |
| 400 | `"hourly bands at hour <i> must be finite numbers between 50 and 90 F"` | Non-finite value at hour `<i>` |
| 400 | `"hourly low must be less than high at hour <i>"` | `hourly_low_temps_f[i] >= hourly_high_temps_f[i]` |
| 401 | `"Not authenticated"` | Missing or invalid token |
| 422 | array `detail` | Any other field out of range or of the wrong type. The entry's `msg` names the rule, for example `"Value error, base_temperature must be between 60 and 85F"`. See [Errors, Limits & Stability](/api-reference/errors-and-limits/#error-shape). |
| 503 | `"Database unavailable"` | Database not reachable |

---

## GET /api/v1/preferences/default-bands

Preview the default hourly comfort band — the band that applies when no hourly arrays are saved — without saving anything. Use this to build a "Reset to defaults" control that shows what will actually apply, or to render the default band before a user has customized it. The result depends only on `base_temperature` and `optimization_mode`.

**Auth:** Required (Bearer JWT)

### Example

```bash
curl "https://api.hungrymachines.io/api/v1/preferences/default-bands?base_temperature=72&optimization_mode=cool" \
  -H "Authorization: Bearer YOUR_TOKEN"
```

Every query param is optional — an omitted one falls back to your stored `/api/v1/preferences` value.

| Param | Type | Validation | Falls back to |
|---|---|---|---|
| `base_temperature` | float | 60.0–85.0 °F | your stored `base_temperature` |
| `optimization_mode` | string | `"cool"`, `"heat"`, or `"auto"` | your stored `optimization_mode` |
| `savings_level` | integer | 1, 2, or 3 | Accepted for compatibility; does not change the result |
| `time_away` | string | HH:MM | Accepted for compatibility; does not change the result |
| `time_home` | string | HH:MM | Accepted for compatibility; does not change the result |

`optimization_mode="auto"` resolves to the cool-shaped band for this preview. `"off"` is not accepted here.

### Response (200)

```json
{
  "hourly_high_temps_f": [73.0, 73.0, "...(24 values)"],
  "hourly_low_temps_f": [70.0, 70.0, "...(24 values)"],
  "peak_hours": [13, 21],
  "precool_hours": [9, 13]
}
```

`hourly_high_temps_f`/`hourly_low_temps_f` are 24-element arrays, hour `0` = midnight local time — the same shape you'd submit as an hourly override above. `peak_hours`/`precool_hours` are fixed `[start, end)` reference windows in local hours, returned so you can shade the usual expensive hours and the hours before them in a UI without hardcoding them. They do not vary with the query params.

### Errors

| Status | Detail | Cause |
|---|---|---|
| 400 | `"base_temperature must be between 60 and 85F"` | `base_temperature` out of range |
| 400 | `"savings_level must be 1, 2, or 3"` | `savings_level` out of range |
| 400 | `"Time must be in HH:MM format"` | `time_away` or `time_home` malformed |
| 400 | `"optimization_mode must be cool, heat, or auto"` | Unknown mode, or `"off"` |
| 401 | `"Not authenticated"` | Missing or invalid token |
| 422 | array `detail` | A query value of the wrong type, for example a non-numeric `base_temperature` |

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