Skip to content

Appliances & Unified Schedules

The appliance endpoints let you register devices (HVAC, EV charger, home battery, water heater, solar, dehumidifier), 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. Which config fields are required depends on the type — each table below marks them. Server-side validation is strict: out-of-range or missing required fields return 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" }
FieldTypeRequiredConstraints
hvac_typestringNo"central_ac", "window_ac", "heat_pump", or "furnace"
home_size_sqftintegerNo100 – 20 000
entity_idstringYesThe climate.* entity that accepts set_temperature and ideally exposes current_temperature.
indoor_temp_entity_idstring | nullNoA sensor.* in °F, used as a fallback when the climate entity reports current_temperature as None (common with Tuya / Smart Life wrappers, IR-blaster controllers, and the Generic Thermostat helper).
FieldTypeRequiredConstraints
battery_capacity_kwhfloatNo0 – 300
max_charge_rate_kwfloatNo0 – 50
efficiencyfloatNo0.5 – 1.0
entity_idstringYesThe switch.* entity the integration toggles to start/stop charging.
soc_entity_idstring | nullNoA sensor.* exposing 0–100 % state of charge. When set, the poller pushes SoC every 5 min.
FieldTypeRequiredConstraints
capacity_kwhfloatNo0 – 300
max_charge_rate_kwfloatNo0 – 50
max_discharge_rate_kwfloatNo0 – 50
entity_idstringYesThe switch / button / select entity that toggles charge mode.
soc_entity_idstring | nullNoA sensor.* for battery SoC %.
FieldTypeRequiredConstraints
tank_size_gallonsintegerNo0 – 200
element_wattsintegerNo0 – 10 000
insulation_factorfloatNo0.01 – 0.05
entity_idstringYesThe switch.* (resistive) or climate.* (smart tank) entity.
temp_entity_idstring | nullNoA sensor.* exposing tank temperature in °F.

Solar is forecast-only — no entity_id, no readings. The system size + orientation discount the effective pricing curve other appliances see during daylight.

FieldTypeRequiredConstraints
system_size_kwfloatNo0 – 100 (DC nameplate)
azimuth_degreesintegerNo0 – 360 (default 180 = south)
tilt_degreesintegerNo0 – 90

Dehumidifiers are data-collection only (v1): no schedule is produced, but the integration records the device’s temp / humidity / power / on-off state.

FieldTypeRequiredConstraints
entity_idstringYesThe humidifier.* entity (Home Assistant models dehumidifiers under the humidifier domain).
indoor_temp_entity_idstringYesA sensor.* exposing co-located indoor temp in °F (a humidifier.* entity has no thermistor).
indoor_humidity_entity_idstring | nullNoA sensor.* for co-located indoor RH. Falls back to the entity’s current_humidity.
power_sensor_entity_idstring | nullNoA sensor.* exposing power draw in watts.
capacity_pints_per_dayfloat | nullNoNameplate spec (0 – 200). Informational.
StatusDetailCause
400"appliance_type must be one of ('hvac', 'ev_charger', 'home_battery', 'water_heater', 'solar', 'dehumidifier')"Unknown type
400"<field>: <message>"Per-type config validation failure (e.g. "entity_id: String should have at least 3 characters")
403"Upgrade required"Free-tier limit (1 HVAC only)

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

PUT /api/v1/appliances/{appliance_id}

Section titled “PUT /api/v1/appliances/”

Update an appliance’s name or config. When updating config, send the full per-type object — partial config patches aren’t supported because the server re-validates the whole shape.

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

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

Errors: 404 "Appliance not found"


DELETE /api/v1/appliances/{appliance_id}

Section titled “DELETE /api/v1/appliances/”

Permanently delete an appliance the caller owns. Cascades through 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:

StatusDetailCause
404"Appliance not found"Doesn’t exist, or owned by another user
503"Failed to delete appliance"Transient DB failure

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

Section titled “POST /api/v1/appliances//readings”

Push sensor data for a specific non-HVAC appliance (EV / battery / water heater).

Auth: Required

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 } }
]
}'
FieldTypeRequiredDescription
timestampstring (ISO 8601)Yes
statestringYesFree-form label, e.g. "CHARGING", "IDLE", "ON", "OFF", "DISCHARGING".
valuefloatYesCharge % (0–100) for EV/battery; tank temperature (60–180 °F) for water heater. Validated per type.
power_wattsfloatNoCurrent power draw
metadataobjectNoAny additional data, stored as sent

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

Rate limit: shared with /api/v1/readings — a runaway-loop safeguard that scales with your active appliance count, not a normal-use limit (see Readings → Rate limit).

StatusDetailCause
400"EV/battery value must be 0-100 (percent)"EV/battery value out of range
400"Value must be 60-180 (degrees F) for water_heater"Water heater value out of range
404"Appliance not found"Bad appliance_id, or not owned by the caller
429"Rate limit exceeded: ..."Combined daily count exceeded

Constraints tell the optimizer the goals for an EV charger, battery, or water heater. (HVAC comfort settings use per-HVAC preferences instead.)

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

Section titled “POST /api/v1/appliances//constraints”

Set optimization constraints. Saving also schedules an immediate debounced re-optimization; the panel uses POST /api/v1/schedule/recompute when it wants to wait synchronously.

Auth: Required

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 }

Response (200): { "status": "ok", "constraints": { "..." } }

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

Section titled “GET /api/v1/appliances//constraints”

Read back the stored constraints so a client can re-display what the user last saved. The appliance-list projection omits the constraints, so this is the read path for EV / battery / water-heater 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" } }

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//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. Unset units default to the values a fresh signup would receive.

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

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

Section titled “PUT /api/v1/appliances//preferences”

Update per-HVAC preferences. All fields optional — the row is upserted, so unspecified fields are preserved. The account-level preferences row is left untouched. optimization_enabled here is the per-unit pause switch (ANDed with the account-level master switch). A successful PUT triggers a debounced recompute.

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 (band validation), 404 "Appliance not found", 422 (Pydantic validation), 503 "Database unavailable".


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

Section titled “GET /api/v1/appliances//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 {}.

Auth: Required

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

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. Both setpoint_temps and temp_trajectory are null on defaults rows — fall back to (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" }
}

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.

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

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

Every per-appliance entry carries an entities sub-object echoing the HA entity ids needed to apply the schedule. Only keys the config actually set are included — entity_id for everything except solar, plus soc_entity_id (EV/battery) or temp_entity_id (water heater) when set. Solar entries have no entities block.

Errors: 404 "Appliance not found"


Unified view of every appliance’s schedule in one response. The endpoint a multi-device integration polls once a 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, "..."],
"temp_trajectory": [72.1, "..."]
},
"savings_pct": 18.5,
"source": "optimization",
"optimization_enabled": true,
"entities": { "entity_id": "climate.living_room" },
"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",
"entities": { "entity_id": "switch.tesla_charger" }
}
],
"optimization_enabled": true,
"integration_health": { "status": "healthy", "message": "Integration is healthy.", "lookback_hours": 6, "appliances": ["..."] }
}

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

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. A paused unit still has its schedule computed, so re-enabling takes effect on the next poll.

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.

TypeSchedule fieldsUnit
hvacintervals (int[48]), high_temps (float[48]), low_temps (float[48]), setpoint_temps (float[48] | null), temp_trajectory (float[48] | null)Fahrenheit
ev_chargerintervals (bool[48]), value_trajectory (float[48]), unitpercent (0–100)
home_batteryintervals (bool[48]), value_trajectory (float[48]), unitpercent (0–100)
water_heaterintervals (bool[48]), temp_trajectory (float[48]), unitfahrenheit
solarNo schedule — contributes to other appliances’ pricing but emits no commands.
dehumidifierNo schedule — data-collection only (v1).

To force a synchronous recompute (e.g. right after the user saves a constraint) and return the same shape plus a results array, use POST /api/v1/schedule/recompute.


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 for 6 hours. Your thermostat is likely disconnected or its entity was renamed.",
"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 }
]
}
FieldTypeNotes
statusstringSee table below
last_reading_atstring | nullISO timestamp of the most recent reading in the window
last_reading_age_secondsint | nullAge of the most recent reading
sample_countintReadings in the lookback window
messagestringShort human-readable description — safe to render verbatim in a banner
lookback_hoursintWindow inspected (currently 6 h)
appliancesarrayPer-HVAC breakdown (appliance_id, name, plus the fields above). Non-HVAC units omitted; empty when no HVAC is registered.
statusMeaning
healthyRecent readings present and indoor temperature varying.
stale_dataLast reading > 30 minutes old. The push pipeline is stalled.
frozen_sensorReadings arriving on cadence but indoor temperature is flatlined. Sensor likely disconnected or the entity renamed.
no_dataZero readings in the window. Integration is offline.

Errors: 401 "Not authenticated"


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 exposes the full audit trail so a UI can show what was chosen night-over-night for the same target day.

Auth: Required

ParamRequiredNotes
appliance_idNoOmit for all appliances the account owns.
startNoISO date (YYYY-MM-DD). Default: end - 14 days.
endNoISO date. 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": ["..."] },
"baseline_cost": 0.42,
"optimized_cost": 0.31,
"savings_pct": 26.1,
"created_at": "2026-05-14T21:35:04Z"
}
]
}
]
}

Entries are sorted by date ascending, then created_at ascending. baseline_cost / optimized_cost (dollars) and savings_pct are display-only. A row may carry additional backend-defined fields — render the documented ones and ignore the rest.

Errors:

StatusDetailCause
400"window exceeds 90 days" / "start must be <= end"Bad date range
401"Not authenticated"Missing or invalid token
404"appliance not found for this user"Only when appliance_id is set