Skip to content

Appliances & Unified Schedules

The appliance endpoints let you register devices (HVAC, EV charger, home battery, water heater, solar, dehumidifier, home robot), push per-device sensor data, set constraints and per-HVAC preferences, and pull schedules and link-health.


Register a new appliance.

Auth: Required

Every registration requires appliance_type, name, and a config object. appliance_type is one of hvac, ev_charger, home_battery, water_heater, solar, dehumidifier, robot, and cannot be changed later. Which config fields are required depends on the type — each table below marks them. A config that breaks a rule returns 400 with the offending field name in detail.

Terminal window
curl -X POST https://api.hungrymachines.io/api/v1/appliances \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"appliance_type": "ev_charger",
"name": "Tesla Model 3",
"config": {
"battery_capacity_kwh": 75,
"max_charge_rate_kw": 7.2,
"efficiency": 0.9,
"entity_id": "switch.tesla_charger",
"soc_entity_id": "sensor.tesla_battery_level"
}
}'

Response (201):

{ "appliance_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" }
Field Type Required Constraints
hvac_type string Yes "central_ac", "window_ac", "heat_pump", "furnace", or "mini_split"
home_size_sqft integer Yes 100 – 20 000
entity_id string Yes Identifier of the thermostat or unit your client commands. At least 3 characters.
indoor_temp_entity_id string | null No Identifier of a separate indoor temperature sensor (°F), for a unit that does not report its own room temperature.
indoor_humidity_entity_id string | null No Identifier of an indoor relative-humidity sensor.
power_sensor_entity_id string | null No Identifier of a sensor reporting the unit’s power draw in watts.
setpoint_min_f integer | null No Lowest setpoint the unit accepts, 40 – 99 °F.
setpoint_max_f integer | null No Highest setpoint the unit accepts, 40 – 99 °F. Must be at least setpoint_min_f. When both are set, every setpoint and comfort-band value the schedule serves is kept inside this range.
Field Type Required Constraints
battery_capacity_kwh float Yes Greater than 0, up to 300
max_charge_rate_kw float Yes Greater than 0, up to 50
efficiency float Yes 0.5 – 1.0
entity_id string Yes Identifier of the charger your client switches on and off. At least 3 characters.
soc_entity_id string | null No Identifier of a sensor reporting state of charge, 0–100 %.
Field Type Required Constraints
capacity_kwh float Yes Greater than 0, up to 300
max_charge_rate_kw float Yes Greater than 0, up to 50
max_discharge_rate_kw float Yes Greater than 0, up to 50
entity_id string Yes Identifier of the control your client uses to switch charge mode. At least 3 characters.
soc_entity_id string | null No Identifier of a sensor reporting state of charge, 0–100 %.

A robot vacuum or mower. Docking stands in for charging: a true slot in the schedule’s intervals means the robot should be on its dock. Your client only ever sends the robot back to its dock; it never undocks it and never starts a task.

Field Type Required Constraints
battery_capacity_kwh float Yes Greater than 0, up to 10
max_charge_rate_kw float Yes Greater than 0, up to 2
entity_id string Yes Identifier of the robot your client docks. At least 3 characters.
soc_entity_id string | null No Identifier of a sensor reporting battery level, 0–100 %.
Field Type Required Constraints
tank_size_gallons integer Yes Greater than 0, up to 200
element_watts integer Yes Greater than 0, up to 10 000
insulation_factor float Yes 0.01 – 0.05
entity_id string Yes Identifier of the heater your client switches on and off. At least 3 characters.
temp_entity_id string | null No Identifier of a sensor reporting tank temperature in °F.

A solar array has no entity_id and receives no commands. Registering it lets the schedules for your other appliances account for daytime generation.

Field Type Required Constraints
system_size_kw float Yes Greater than 0, up to 100 (DC nameplate)
azimuth_degrees integer No 0 – 360. Default 180 (south).
tilt_degrees integer No 0 – 90. Default 20.
production_sensor_entity_id string | null No Identifier of a sensor reporting the array’s production. If you have one, push production in watts through the appliance readings route below to improve the generation forecast.

Dehumidifiers are recorded only: no schedule is produced. Push their readings through POST /api/v1/readings with the dehumidifier’s appliance_id, using hvac_state "DRY" (running) or "OFF" (idle).

Field Type Required Constraints
entity_id string Yes Identifier of the dehumidifier. At least 3 characters.
indoor_temp_entity_id string Yes Identifier of a room temperature sensor (°F) next to the unit. At least 3 characters.
indoor_humidity_entity_id string | null No Identifier of a room relative-humidity sensor.
power_sensor_entity_id string | null No Identifier of a sensor reporting power draw in watts.
capacity_pints_per_day float | null No Nameplate capacity, greater than 0, up to 200. Informational.
Status Detail Cause
400 "<field>: <message>" Per-type config validation failure, e.g. "entity_id: Field required" or "entity_id: String should have at least 3 characters"
403 "Upgrade required" Free-tier limit (1 HVAC only). Checked before the config.
422 array detail appliance_type is not one of the seven types, or appliance_type / name is missing
503 "Database unavailable" or "Failed to create appliance" The appliance could not be saved

List all registered appliances.

Auth: Required

Terminal window
curl https://api.hungrymachines.io/api/v1/appliances \
-H "Authorization: Bearer YOUR_TOKEN"

Response (200):

[
{
"id": "a1b2c3d4-...",
"user_id": "550e8400-...",
"appliance_type": "ev_charger",
"name": "Tesla Model 3",
"config": {
"battery_capacity_kwh": 75,
"max_charge_rate_kw": 7.2,
"efficiency": 0.9,
"entity_id": "switch.tesla_charger",
"soc_entity_id": "sensor.tesla_battery_level"
},
"is_active": true,
"created_at": "2025-11-15T10:30:00+00:00",
"optimization_enabled": true
}
]

optimization_enabled is the per-appliance pause flag for non-HVAC types; false means paused. HVAC units are paused through per-HVAC preferences.

Status Detail Cause
503 "Database unavailable" The appliance list could not be read

Update an appliance’s name, config, or pause flag. All three fields are optional; send only what changes. When updating config, send the full per-type object, including every required key — config replaces what is stored. appliance_type cannot be changed.

Auth: Required

Terminal window
curl -X PUT https://api.hungrymachines.io/api/v1/appliances/a1b2c3d4-... \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Tesla Model 3 (Garage)",
"config": { "battery_capacity_kwh": 75, "max_charge_rate_kw": 11.5, "efficiency": 0.9, "entity_id": "switch.tesla_charger", "soc_entity_id": "sensor.tesla_battery_level" }
}'
Field Type Required Description
name string No New display name
config object No The complete config for the appliance’s type
optimization_enabled boolean No Pause (false) or resume (true) a non-HVAC appliance. HVAC units are paused through per-HVAC preferences.

Response (200): Full appliance object (same shape as the GET list item).

Errors:

Status Detail Cause
400 "<field>: <message>" Config validation failure, e.g. "entity_id: Field required"
404 "Appliance not found" Doesn’t exist, or owned by another user
422 array detail A field of the wrong JSON type
503 "Database unavailable" or "Failed to update appliance" The change could not be saved

Permanently delete an appliance the caller owns, along with its readings, schedules, and constraints.

Auth: Required

Terminal window
curl -X DELETE https://api.hungrymachines.io/api/v1/appliances/a1b2c3d4-... \
-H "Authorization: Bearer YOUR_TOKEN"

Response (204): No body.

Errors:

Status Detail Cause
404 "Appliance not found" Doesn’t exist, or owned by another user
503 "Database unavailable" or "Failed to delete appliance" The appliance could not be deleted

POST /api/v1/appliances/{appliance_id}/readings

Section titled “POST /api/v1/appliances/'{appliance_id}'/readings”

Push state readings for a specific non-HVAC appliance (EV charger, home battery, robot, water heater, solar). HVAC and dehumidifier readings go through POST /api/v1/readings instead.

Auth: Required

readings is required and holds 1 to 100 readings. In each reading, timestamp, state, and value are required; power_watts and metadata are optional.

Terminal window
curl -X POST https://api.hungrymachines.io/api/v1/appliances/a1b2c3d4-.../readings \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"readings": [
{ "timestamp": "2025-11-18T14:30:00+00:00", "state": "CHARGING", "value": 45.2, "power_watts": 7200.0, "metadata": { "voltage": 240, "current": 30 } }
]
}'
Field Type Required Description
timestamp string (ISO 8601) Yes
state string Yes Free-form label, e.g. "CHARGING", "IDLE", "ON", "OFF", "DISCHARGING".
value float Yes Charge % (0–100) for EV charger, home battery and robot; tank temperature (60–180 °F) for water heater; production watts (0–100000) for solar. Validated per type.
power_watts float No Current power draw. For solar, 0–100000.
metadata object No Any additional data, stored as sent

A batch is accepted or rejected as a whole.

Response (201): { "accepted": 1 }

Daily limit: an account can push 300 readings per UTC day for each active EV charger, home battery, water heater, solar array or robot it has registered (floor of one device’s worth), counted across all of those appliances together. A batch that would take the day’s total past the cap is rejected whole with 429. One appliance pushing every 5 minutes is 288 a day, comfortably under its 300 share — register more appliances and the cap grows with them. This limit is separate from the one on POST /api/v1/readings.

Status Detail Cause
400 "Value must be 0-100 (percent) for ev_charger" Charge value out of range (also ... for home_battery, ... for robot)
400 "Value must be 60-180 (degrees F) for water_heater" Water heater value out of range
400 "Value must be 0-100000 (watts) for solar" Solar value out of range
400 "power_watts must be 0-100000 (watts) for solar" Solar power_watts out of range
404 "Appliance not found" Bad appliance_id, or not owned by the caller
422 array detail Empty batch, more than 100 readings, or a missing or mistyped field
429 "Rate limit exceeded: max <cap> readings per user per day (scaled to <N> active appliance(s))" The account’s daily total for this route would pass its scaled cap
503 "Database unavailable" or "Failed to write readings" The readings could not be saved

Constraints are the limits an EV charger, home battery, robot, or water heater is scheduled within. (HVAC comfort settings use per-HVAC preferences instead.)

POST /api/v1/appliances/{appliance_id}/constraints

Section titled “POST /api/v1/appliances/'{appliance_id}'/constraints”

Set the constraints. The body is a JSON object that replaces the stored constraints, so send the complete set each time. Saving also triggers a recompute of the schedule shortly afterwards; use POST /api/v1/schedule/recompute when you want to wait for the result.

An EV charger, home battery or robot gets no schedule until constraints have been saved. A water heater is scheduled on the defaults below until you set its own.

Auth: Required

Every key is optional; the default applies when a key is omitted. Keys are validated when present, per appliance type — a key for a type not listed below is stored but never checked, and a validation failure is a 400 naming the field.

Type Key Type Required Description
EV charger, home battery target_charge_pct number No Charge to reach by the deadline, 0–100. Default 80.
EV charger, home battery min_charge_pct number No Charge never to drop below, 0–100, and never above target_charge_pct when both are sent. Default 20.
EV charger, home battery deadline_time string No HH:MM, 24-hour local time (a real hour and minute). Default "23:59".
EV charger, home battery current_charge_pct number No Present charge, 0–100. Default 50. Used only when no recent state-of-charge reading has been pushed.
Robot target_charge_pct number No Charge to reach before the tasks window, 0–100. Default 90.
Robot min_charge_pct number No Charge never to drop below, 0–100, and never above target_charge_pct when both are sent. Default 25.
Robot current_charge_pct number No Present charge, 0–100. Default 50. Used only when no recent battery reading has been pushed.
Robot tasks_window_start / tasks_window_end string No HH:MM, 24-hour local time (a real hour and minute). Defaults "09:00" / "17:00". The window is half-open and may wrap midnight; the robot is expected off its dock during it, and charging is scheduled around it.
Water heater min_temp_f number No Lowest tank temperature, °F, 60–180. Default 110.
Water heater max_temp_f number No Highest tank temperature, °F, 60–180. Must be above min_temp_f when both are sent. Default 140.

EV Charger:

{ "target_charge_pct": 80, "min_charge_pct": 30, "deadline_time": "07:00", "current_charge_pct": 35 }

Home Battery:

{ "target_charge_pct": 90, "min_charge_pct": 20, "deadline_time": "23:59" }

Water Heater:

{ "max_temp_f": 140, "min_temp_f": 110 }

Home Robot:

{ "target_charge_pct": 90, "min_charge_pct": 25, "current_charge_pct": 50, "tasks_window_start": "09:00", "tasks_window_end": "17:00" }

Response (200): { "status": "ok", "constraints": { "..." } } — constraints echoes the object you sent.

Errors: 400 "<field>: <message>" (a key in the table above failed validation for this appliance’s type), 404 "Appliance not found", 422 (the body is not a JSON object; array detail), 503 "Database unavailable" or 503 "Failed to save constraints".

GET /api/v1/appliances/{appliance_id}/constraints

Section titled “GET /api/v1/appliances/'{appliance_id}'/constraints”

Read back the stored constraints so a client can re-display what the user last saved. The appliance list does not include constraints, so this is the read path for EV / battery / water-heater / robot constraints.

Auth: Required

Response (200): constraints is {} when none have been set.

{ "status": "ok", "constraints": { "target_charge_pct": 80, "min_charge_pct": 30, "deadline_time": "07:00" } }

Errors: 404 "Appliance not found", 503 "Database unavailable".


Each HVAC unit carries its own comfort/savings/mode/pause preferences, so a two-HVAC home holds independent setpoints per unit. The shape mirrors the HVAC-relevant subset of /api/v1/preferences; account-level fields (like newsletter_opt_in) stay on the account endpoint.

GET /api/v1/appliances/{appliance_id}/preferences

Section titled “GET /api/v1/appliances/'{appliance_id}'/preferences”

Auth: Required

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

Field semantics match the corresponding fields on GET /api/v1/preferences. A unit whose preferences have never been saved returns the account’s own current preferences (the standard defaults shown above only if the account hasn’t customized them either) — the same values planning for that unit follows until the unit’s own preferences are saved.

Errors: 404 "Appliance not found" (missing, not owned, or not an HVAC unit), 503 "Database unavailable".

PUT /api/v1/appliances/{appliance_id}/preferences

Section titled “PUT /api/v1/appliances/'{appliance_id}'/preferences”

Update per-HVAC preferences. All fields optional — fields you do not send keep their saved values. The account-level preferences are left untouched. optimization_enabled here is the per-unit pause switch: a unit is optimized only when both it and the account-level master switch are true. A successful PUT triggers a debounced recompute.

The first save for a unit makes that unit’s own preferences authoritative for every field. On that first PUT, a field you omit is filled in from your current account-level preferences at that moment — not the standard default shown under GET. A later change to your account-level preferences does not retroactively change the unit’s own row; send a PUT to this endpoint to update it.

Auth: Required

Terminal window
curl -X PUT https://api.hungrymachines.io/api/v1/appliances/a1b2c3d4-.../preferences \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "base_temperature": 70.0, "savings_level": 3, "optimization_mode": "auto", "optimization_enabled": false }'

Validation matches PUT /api/v1/preferences.

Response (200): Full per-appliance preferences object (same shape as GET).

Errors: 400 (hourly-band validation, string detail), 404 "Appliance not found", 422 (any other field out of range or of the wrong type, array detail), 503 "Database unavailable".

GET /api/v1/appliances/{appliance_id}/preferences/default-bands

Section titled “GET /api/v1/appliances/'{appliance_id}'/preferences/default-bands”

Same preview as GET /api/v1/preferences/default-bands, scoped to this appliance. An omitted query param falls back to this unit’s own preferences once they have been saved, otherwise to the account-level preferences.

Auth: Required

Query params, validation, and the response shape are identical to the account-level endpoint.

Errors: 400 (same validation messages), 404 "Appliance not found", 422 (a query value of the wrong type), 503 "Database unavailable".


GET /api/v1/appliances/{appliance_id}/schedule

Section titled “GET /api/v1/appliances/'{appliance_id}'/schedule”

Get the schedule for a single appliance. Same per-type schedule blob as one entry of GET /api/v1/schedules, plus the date. When none exists yet, source is "defaults", savings_pct is 0.0, and schedule is {}. date is today in the account’s local timezone.

Auth: Required — a user access token.

Every response also carries optimization_enabled: the effective pause flag for this appliance, with the account’s master switch already folded in. It is present on every appliance type. Do not apply the schedule while it is false. A response adds "stale": true when the schedule returned is from an earlier day because none has been produced for today.

All schedules use 48 × 30-minute intervals.

{
"appliance_id": "...",
"appliance_type": "hvac",
"name": "Living Room AC",
"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)"]
},
"savings_pct": 18.5,
"source": "optimization",
"entities": { "entity_id": "climate.living_room", "indoor_temp_entity_id": "sensor.back_bedroom_temperature" },
"optimization_enabled": true
}

Apply setpoint_temps[idx] at each 30-min boundary regardless of HVAC mode. high_temps/low_temps are the display-only comfort band; temp_trajectory is the display-only predicted line. An appliance with no stored plan yet returns source: "defaults" with an empty schedule object: there is nothing to apply, so leave the device as it is and check again after the next plan. On a stored plan read each key defensively; if setpoint_temps is missing but the band is present, use (high_temps[idx] + low_temps[idx]) / 2. HVAC entries also carry an inline integration_health block (see health).

{
"appliance_id": "...",
"appliance_type": "ev_charger",
"name": "Tesla Model 3",
"date": "2025-11-18",
"schedule": {
"intervals": [false, false, "...", true, true, "...", false],
"value_trajectory": [35.0, 35.0, "...", 57.5, 80.0, "...", 80.0],
"unit": "percent"
},
"savings_pct": 32.1,
"source": "optimization",
"entities": { "entity_id": "switch.tesla_charger", "soc_entity_id": "sensor.tesla_battery_level" },
"optimization_enabled": true
}

intervals: boolean — true = charger on. value_trajectory: display-only predicted charge level (0–100 %).

Same shape as EV charger: intervals (boolean), value_trajectory (percent), unit: "percent", plus entities.

Same shape as EV charger: intervals (boolean), value_trajectory (percent), unit: "percent", plus entities. intervals[idx] = true means “should be on its dock” — docking is the charging proxy, since no robot exposes charge control directly.

{
"appliance_id": "...",
"appliance_type": "water_heater",
"name": "Basement Water Heater",
"date": "2025-11-18",
"schedule": {
"intervals": [true, true, false, "...", false, true, "..."],
"temp_trajectory": [120.0, 128.5, 135.2, "...", 118.3, 126.7, "..."],
"unit": "fahrenheit"
},
"savings_pct": 22.0,
"source": "optimization",
"entities": { "entity_id": "switch.water_heater", "temp_entity_id": "sensor.water_heater_temperature" },
"optimization_enabled": true
}

intervals: boolean — true = element on. temp_trajectory: display-only predicted tank temperature.

Every per-appliance entry carries an entities sub-object echoing the identifier keys from the appliance’s config, so your client knows which device to command. Only keys the config actually set are included: entity_id for everything except solar, plus soc_entity_id, temp_entity_id, indoor_temp_entity_id or production_sensor_entity_id when set. It is {} when none are set, as for a solar array with no production sensor.

Errors: 401 (missing, invalid or expired token), 404 "Appliance not found", 503 "Database unavailable".


Unified view of every appliance’s schedule in one response, and the route that carries the per-slot mode and fan schedules. Pull it after the overnight run, then poll GET /api/v1/schedules/updated-at and pull again whenever its value changes: a plan for today can be replaced during the day.

Auth: Required

Terminal window
curl https://api.hungrymachines.io/api/v1/schedules \
-H "Authorization: Bearer YOUR_TOKEN"

Response (200):

{
"date": "2025-11-18",
"appliances": [
{
"appliance_id": "...",
"appliance_type": "hvac",
"name": "Living Room AC",
"schedule": {
"intervals": [0, 1, "...", 47],
"high_temps": [74.0, "..."],
"low_temps": [70.0, "..."],
"setpoint_temps": [72.0, "..."],
"room_target_temps": [72.0, "..."],
"temp_trajectory": [72.1, "..."],
"mode": "cool",
"generated_at": "2025-11-18T04:15:00+00:00"
},
"savings_pct": 18.5,
"source": "optimization",
"optimization_enabled": true,
"entities": { "entity_id": "climate.living_room", "indoor_temp_entity_id": "sensor.back_bedroom_temperature" },
"integration_health": { "status": "healthy", "message": "Integration is healthy.", "lookback_hours": 6 }
},
{
"appliance_id": "...",
"appliance_type": "ev_charger",
"name": "Tesla Model 3",
"schedule": { "intervals": [false, "...", true, "...", false], "value_trajectory": [30.0, "...", 80.0], "unit": "percent" },
"savings_pct": 32.1,
"source": "optimization",
"optimization_enabled": true,
"entities": { "entity_id": "switch.tesla_charger" }
}
],
"optimization_enabled": true,
"integration_health": { "status": "healthy", "message": "Integration is healthy.", "lookback_hours": 6, "appliances": ["..."] },
"divergence_summary": { "verdict": "healthy", "human_readable": "All three signals agree — recent readings are clean.", "sample_count": 288 }
}

Each entry in appliances uses the per-type schedule shape above.

Field Type Notes
appliance_id, appliance_type, name string Identify the appliance and select the schedule shape.
schedule object The stored plan, or {} when there is none.
savings_pct float Display-only. 0.0 when there is no stored plan.
source string "optimization" (a stored plan) or "defaults" (no stored plan yet).
stale boolean Present, and true, only when there is no plan for today and the most recent stored plan is returned instead. Absent otherwise.
optimization_enabled boolean Effective per-appliance pause flag. On every entry, of every type.
entities object Entity ids from the appliance’s config. Always present; {} when none are set.
integration_health object HVAC entries only.

No stored plan (source: "defaults"). An appliance that has not been planned yet returns "schedule": {}, "savings_pct": 0.0 and "source": "defaults". There is no comfort band and no setpoint on this route to fall back to, so send that appliance no commands and leave it under its own control. If you want a default band to show or hold for an HVAC unit, call GET /api/v1/schedule?appliance_id=…, which returns one. The first plan is stored by the first recompute (a settings save, or POST /api/v1/schedule/recompute) or the next overnight run.

Present or absent, never null. On this route schedule is the stored plan as written, so a schedule key is either present with a value or absent. Check for the key before reading it.

Field Type Notes
intervals int[48] Always [0, 1, …, 47].
high_temps, low_temps float[48] The comfort band (°F). Display-only.
setpoint_temps float[48] The thermostat command. Apply setpoint_temps[idx] at each 30-minute boundary.
room_target_temps float[48] The room temperature the plan aims to hold in each slot (°F). It usually equals setpoint_temps; the two differ where the unit’s own thermostat reads differently from the room. A client that reads a separate room sensor may adjust the setpoint to steer the room toward this value, and should then report it as room_target on its readings. Otherwise ignore it and apply setpoint_temps.
hvac_mode_schedule string[48] Per-slot mode: "OFF", "COOL" or "HEAT". When present, apply it at each boundary along with the setpoint. Present when the unit’s optimize_hvac_mode preference is on, always for a unit registered with hvac_type: "window_ac", and on a warm-up day. It can be absent on any given plan; when absent, do not change the unit’s mode.
fan_mode_schedule string[48] Per-slot fan speed: "low", "high" or "auto". "auto" (and "off") means “do not send a fan command” for that slot; send one only for "low" or "high", and only when the unit has a matching fan option. Present when the unit’s optimize_hvac_fan preference is on and the unit has reported distinct fan speeds in its readings, always for hvac_type: "window_ac", and on a warm-up day. When absent, do not change the fan.
temp_trajectory float[48] Predicted indoor temperature, display-only. Absent on a warm-up day.
mode string "cool" or "heat".
generated_at string UTC timestamp of when the plan was produced.

An HVAC schedule can carry further keys. They are display-only and may change without notice; ignore any key not listed here.

optimization_enabled (top level): the master pause switch. When false, do not apply any appliance’s schedule until a later poll returns true.

optimization_enabled (per appliance): the effective per-unit pause flag (already folds in the master switch). The apply loop should gate on this per appliance. It is on every entry, of every type. A paused unit still has a schedule entry, but do not apply it while the flag is false.

integration_health: an inline link-health verdict so you can show a banner when data stops flowing — same payload as GET /api/v1/integration/health, included here to save a round-trip. HVAC entries carry their own inline block; non-HVAC entries omit it.

divergence_summary: a compact, display-only verdict comparing what your client reported commanding against what the thermostat reports: verdict (healthy, entity_unreliable, thermostat_ignoring_commands, commanded_missing, no_data or unknown), human_readable (a short description to show) and sample_count. It depends on the commanded_* fields your client sends with its readings; a client that sends none gets commanded_missing.

Type Schedule fields Unit
hvac intervals (int[48]), high_temps (float[48]), low_temps (float[48]), setpoint_temps (float[48]), room_target_temps (float[48]), hvac_mode_schedule and fan_mode_schedule (string[48], when they apply), temp_trajectory (float[48]), mode, generated_at — see HVAC schedule fields Fahrenheit
ev_charger intervals (bool[48]), value_trajectory (float[48]), unit, plus display-only min_value, target_value (percent) and deadline_interval (slot index) percent (0–100)
home_battery intervals (bool[48]), value_trajectory (float[48]), unit, plus display-only min_value, target_value, deadline_interval percent (0–100)
robot intervals (bool[48]), value_trajectory (float[48]), unit, plus display-only min_value, target_value, deadline_interval percent (0–100) — true means “should be on its dock”
water_heater intervals (bool[48]), temp_trajectory (float[48]), unit, plus display-only high_temps and low_temps (float[48], the tank’s allowed range) fahrenheit
solar generation_kw_48 (float[48], this array’s own curve), unit, source ("learned", "irradiance" or "nameplate" — a display label for what the generation forecast is based on), self_consumed_kw_48 (float[48]), surplus_kw_48 (float[48]), generation_kwh, self_consumed_kwh, surplus_kwh, export_value_cents (float or null — null with no export-rate curve), generated_at — a forecast to display; there are no per-interval commands kW
dehumidifier No schedule — data-collection only (v1). The entry’s schedule is {}. —

Most stored plans also carry pricing_cents_48 (float[48]), the price curve in cents/kWh the plan was made against, for display.

The entities block is always present. It holds only the keys the appliance’s config set — entity_id, soc_entity_id, temp_entity_id, indoor_temp_entity_id, or production_sensor_entity_id on a solar array — and is {} when none are set.

To recompute now (e.g. right after the user saves a constraint) and get the same appliances entries plus a results array, use POST /api/v1/schedule/recompute. Its response does not carry the top-level optimization_enabled, integration_health or divergence_summary.


A link-health probe over the recent reading stream, so a dashboard can show a banner when the integration silently stops delivering real data. Returns the same block that GET /api/v1/schedules inlines — poll this on a faster cadence (e.g. every 30 s) without pulling the full schedule payload.

Auth: Required

Response (200):

{
"status": "frozen_sensor",
"last_reading_at": "2026-05-29T02:00:00+00:00",
"last_reading_age_seconds": 312,
"sample_count": 71,
"message": "Readings are arriving on time but indoor temperature has been stuck at 70.0°F for 6 hours. Your thermostat is likely disconnected, or your client's sensor mapping has changed.",
"lookback_hours": 6,
"appliances": [
{ "appliance_id": "1111...", "name": "Living Room AC", "status": "healthy", "message": "Integration is healthy.", "lookback_hours": 6 },
{ "appliance_id": "2222...", "name": "Bedroom AC", "status": "frozen_sensor", "message": "...", "lookback_hours": 6 }
]
}
Field Type Notes
status string See table below
last_reading_at string | null ISO timestamp of the most recent reading in the window
last_reading_age_seconds int | null Age of the most recent reading
sample_count int Readings in the lookback window. Readings from a paused appliance are left out
message string Short human-readable description, written to name no product or platform — safe for any client to render verbatim, or key its own wording on status instead.
lookback_hours int Window inspected (currently 6 h)
appliances array Per-HVAC breakdown (appliance_id, name, plus the fields above). Non-HVAC units omitted; empty when no HVAC is registered.
status Meaning
healthy Recent readings present and indoor temperature varying.
stale_data Last reading > 30 minutes old. Readings have stopped arriving.
frozen_sensor Readings arriving on cadence but indoor temperature is flatlined. Sensor likely disconnected or the entity renamed.
no_data Zero readings in the window. The client is offline. Also returned, with an explanatory message, when recent readings could not be read.

This route answers 200 with a status in every case; it has no error status of its own beyond the auth errors.

Errors: 401 ("Not authenticated", "Invalid token" or "Token expired").


Return the history of schedule rows for the account’s appliances in a date range. GET /schedule and /schedules always return the latest schedule per (appliance, date); this returns every stored one, so a UI can show how the plan for a day changed.

Auth: Required

Param Required Notes
appliance_id No Omit for all appliances the account owns.
start No Date, exactly YYYY-MM-DD. Default: end - 14 days.
end No Date, exactly YYYY-MM-DD. Default: today.

end - start must be ≤ 90 days.

Response (200):

{
"start": "2026-05-15",
"end": "2026-05-29",
"appliances": [
{
"appliance_id": "0a0feccb-...",
"appliance_type": "hvac",
"name": "Bedroom AC",
"history": [
{
"id": 79,
"appliance_id": "0a0feccb-...",
"date": "2026-05-14",
"schedule": { "setpoint_temps": ["..."], "temp_trajectory": ["..."] },
"constraints_used": { "optimization_mode": "cool", "base_temperature": 72.0 },
"baseline_cost": 0.42,
"optimized_cost": 0.31,
"savings_pct": 26.1,
"created_at": "2026-05-14T21:35:04Z"
}
]
}
]
}

Each entry carries id, appliance_id, date, schedule (the same object GET /api/v1/schedules returns for that appliance type), constraints_used, baseline_cost, optimized_cost, savings_pct and created_at. Entries are sorted by date ascending, then created_at ascending, so the last entry for a date is the one the schedule routes serve. baseline_cost / optimized_cost (dollars) and savings_pct are display-only.

constraints_used is a fixed, documented field set: whichever of optimization_mode, base_temperature, savings_level, max_temp_f, min_charge_pct, target_charge_pct, deadline_time, tasks_window_start, tasks_window_end and nameplate_kw apply to this appliance’s type (present when a value was recorded for this run, absent otherwise). Ignore any key not in this list.

Errors:

Status Detail Cause
400 "window exceeds 90 days" / "start must be <= end" Bad date range
400 "start must be an ISO date (YYYY-MM-DD)" / "end must be an ISO date (YYYY-MM-DD)" start/end is not a valid ISO date
401 "Not authenticated", "Invalid token" or "Token expired" No Authorization header, or a bad or expired token
404 "appliance not found for this user" Only when appliance_id is set

Lightweight freshness check: returns the latest timestamp a schedule was written for the account, for today, without downloading the schedules themselves. This is how a client notices a daytime re-plan. Poll it every few minutes, keep the last value you saw, and fetch GET /api/v1/schedules again when it changes.

Auth: Required

Response (200):

{ "updated_at": "2026-09-05T14:32:07.123456+00:00" }

updated_at is null when the account has no appliances, or nothing has been scheduled for today yet.

Errors:

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