Skip to content

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. 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 and fetch again when its value changes.

Auth: Required (Bearer JWT)

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.
Terminal window
curl "https://api.hungrymachines.io/api/v1/schedule?appliance_id=a1b2c3d4-..." \
-H "Authorization: Bearer YOUR_TOKEN"
{
"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"
}

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)

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.

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:

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

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:

{
"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"
}
  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 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].
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

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) and a results array reporting each active appliance’s outcome for this run. No request body.

Auth: Required (Bearer JWT)

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

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.

{
"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.

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.

  • 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.

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