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.
POST /api/v1/appliances
Section titled “POST /api/v1/appliances”Register a new appliance.
Auth: Required
Required vs. optional
Section titled “Required vs. optional”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.
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" }Config shapes by type
Section titled “Config shapes by type”| Field | Type | Required | Constraints |
|---|---|---|---|
hvac_type | string | No | "central_ac", "window_ac", "heat_pump", or "furnace" |
home_size_sqft | integer | No | 100 – 20 000 |
entity_id | string | Yes | The climate.* entity that accepts set_temperature and ideally exposes current_temperature. |
indoor_temp_entity_id | string | null | No | A 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). |
EV Charger
Section titled “EV Charger”| Field | Type | Required | Constraints |
|---|---|---|---|
battery_capacity_kwh | float | No | 0 – 300 |
max_charge_rate_kw | float | No | 0 – 50 |
efficiency | float | No | 0.5 – 1.0 |
entity_id | string | Yes | The switch.* entity the integration toggles to start/stop charging. |
soc_entity_id | string | null | No | A sensor.* exposing 0–100 % state of charge. When set, the poller pushes SoC every 5 min. |
Home Battery
Section titled “Home Battery”| Field | Type | Required | Constraints |
|---|---|---|---|
capacity_kwh | float | No | 0 – 300 |
max_charge_rate_kw | float | No | 0 – 50 |
max_discharge_rate_kw | float | No | 0 – 50 |
entity_id | string | Yes | The switch / button / select entity that toggles charge mode. |
soc_entity_id | string | null | No | A sensor.* for battery SoC %. |
Water Heater
Section titled “Water Heater”| Field | Type | Required | Constraints |
|---|---|---|---|
tank_size_gallons | integer | No | 0 – 200 |
element_watts | integer | No | 0 – 10 000 |
insulation_factor | float | No | 0.01 – 0.05 |
entity_id | string | Yes | The switch.* (resistive) or climate.* (smart tank) entity. |
temp_entity_id | string | null | No | A 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.
| Field | Type | Required | Constraints |
|---|---|---|---|
system_size_kw | float | No | 0 – 100 (DC nameplate) |
azimuth_degrees | integer | No | 0 – 360 (default 180 = south) |
tilt_degrees | integer | No | 0 – 90 |
Dehumidifier
Section titled “Dehumidifier”Dehumidifiers are data-collection only (v1): no schedule is produced, but the integration records the device’s temp / humidity / power / on-off state.
| Field | Type | Required | Constraints |
|---|---|---|---|
entity_id | string | Yes | The humidifier.* entity (Home Assistant models dehumidifiers under the humidifier domain). |
indoor_temp_entity_id | string | Yes | A sensor.* exposing co-located indoor temp in °F (a humidifier.* entity has no thermistor). |
indoor_humidity_entity_id | string | null | No | A sensor.* for co-located indoor RH. Falls back to the entity’s current_humidity. |
power_sensor_entity_id | string | null | No | A sensor.* exposing power draw in watts. |
capacity_pints_per_day | float | null | No | Nameplate spec (0 – 200). Informational. |
Errors
Section titled “Errors”| Status | Detail | Cause |
|---|---|---|
| 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) |
GET /api/v1/appliances
Section titled “GET /api/v1/appliances”List all registered appliances.
Auth: Required
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
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
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 | "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
Required vs. optional
Section titled “Required vs. optional”timestamp, state, and value are required; power_watts and metadata are optional.
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/battery; tank temperature (60–180 °F) for water heater. Validated per type. |
power_watts | float | No | Current power draw |
metadata | object | No | Any 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).
Errors
Section titled “Errors”| Status | Detail | Cause |
|---|---|---|
| 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 (non-HVAC appliances)
Section titled “Constraints (non-HVAC appliances)”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" } }Per-HVAC Preferences
Section titled “Per-HVAC Preferences”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
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).
EV Charger
Section titled “EV Charger”{ "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 %).
Home Battery
Section titled “Home Battery”Same shape as EV charger: intervals (boolean), value_trajectory (percent), unit: "percent", plus entities.
Water Heater
Section titled “Water Heater”{ "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.
The entities block
Section titled “The entities block”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"
GET /api/v1/schedules
Section titled “GET /api/v1/schedules”Unified view of every appliance’s schedule in one response. The endpoint a multi-device integration polls once a day.
Auth: Required
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.
Per-type schedule summary
Section titled “Per-type schedule summary”| Type | Schedule fields | Unit |
|---|---|---|
hvac | intervals (int[48]), high_temps (float[48]), low_temps (float[48]), setpoint_temps (float[48] | null), temp_trajectory (float[48] | null) | Fahrenheit |
ev_charger | intervals (bool[48]), value_trajectory (float[48]), unit | percent (0–100) |
home_battery | intervals (bool[48]), value_trajectory (float[48]), unit | percent (0–100) |
water_heater | intervals (bool[48]), temp_trajectory (float[48]), unit | fahrenheit |
solar | No schedule — contributes to other appliances’ pricing but emits no commands. | — |
dehumidifier | No 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.
GET /api/v1/integration/health
Section titled “GET /api/v1/integration/health”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 } ]}| 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 |
message | string | Short human-readable description — safe to render verbatim in a banner |
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. The push pipeline is stalled. |
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. Integration is offline. |
Errors: 401 "Not authenticated"
GET /api/v1/schedules/history
Section titled “GET /api/v1/schedules/history”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
Query parameters
Section titled “Query parameters”| Param | Required | Notes |
|---|---|---|
appliance_id | No | Omit for all appliances the account owns. |
start | No | ISO date (YYYY-MM-DD). Default: end - 14 days. |
end | No | ISO 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:
| Status | Detail | Cause |
|---|---|---|
| 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 |