Readings
The readings endpoint accepts HVAC sensor data from your thermostat or Home Assistant integration. Push a reading every 5 minutes (or batch several) so Hungry Machines can learn how your home responds and produce accurate schedules.
POST /api/v1/readings
Section titled “POST /api/v1/readings”Auth: Required — a user access token.
Required vs. optional
Section titled “Required vs. optional”Only three fields are required on each reading: timestamp, indoor_temp, and hvac_state. Everything else is optional — but the more you send, the more accurate your schedules become. Send whatever your device exposes; there’s no penalty for omitting a field you don’t have.
Request body
Section titled “Request body”{ "readings": [ { "timestamp": "2025-11-18T14:30:00+00:00", "indoor_temp": 72.5, "hvac_state": "COOL", "indoor_humidity": 45.0, "outdoor_temp": 85.0, "target_temp": 72.0, "fan_mode": "auto", "power_watts": 298.0, "hvac_action": "cooling", "commanded_hvac_mode": "COOL", "commanded_fan_mode": "low", "commanded_setpoint": 71.0, "indoor_source": "sensor", "override_active": false, "room_target": 73.0, "appliance_id": "11111111-1111-1111-1111-111111111111" } ], "client": { "kind": "hacs", "version": "3.7.0", "node_bundle": null, "ha_version": "2026.8.1" }}Fields
Section titled “Fields”| Field | Type | Required | Constraints / notes |
|---|---|---|---|
readings |
array | Yes | 1–100 reading objects (below) |
client |
object | No | Identifies the client software. A third-party client should send kind: "custom", optionally with name — see Client identity. |
Per-reading fields
Section titled “Per-reading fields”| Field | Type | Required | Constraints / notes |
|---|---|---|---|
timestamp |
string (ISO 8601) | Yes | UTC recommended |
indoor_temp |
float | Yes | 30.0–120.0 °F |
hvac_state |
string | Yes | "HEAT", "COOL", "ECO", "DRY", "OFF", or "FAN". Case-insensitive. Common aliases ("cooling", "heating", "eco_cool", "dehumidify", "fan_only", "idle") are normalized server-side. "auto" and "heat_cool" say what the unit may do, not what it is doing, and are recorded as "OFF". Any other value is rejected with 422. "ECO" and "DRY" are tracked separately from "COOL". |
indoor_humidity |
float | No | Percent (0–100) |
outdoor_temp |
float | No | Fahrenheit |
target_temp |
float | No | Current thermostat setpoint |
fan_mode |
string | No | Fan-speed label, e.g. "auto", "low", "high". Stored as sent. |
power_watts |
float | No | Current power draw. Send only if the meter measures this specific unit — a shared/whole-home meter can mislead the model. |
hvac_action |
string | No | The climate entity’s hvac_action attribute ("cooling", "heating", "idle", "off", …) — whether the unit is actively conditioning right now, as opposed to its standing mode. Send when available; improves accuracy. |
commanded_hvac_mode |
string | No | The mode your integration last commanded ("COOL"/"HEAT"/"OFF"). Send when available; improves accuracy. |
commanded_fan_mode |
string | No | The fan label your integration last commanded. |
commanded_setpoint |
float | No | The setpoint (°F) your integration last commanded. |
indoor_source |
string | No | Which source produced indoor_temp: "sensor" if you read a separate room-temperature sensor, "entity" if you used the thermostat’s own reading. Any other value is rejected with 422. Send it when you know. |
override_active |
boolean | No | true if your client’s own local override, rather than the schedule, was commanding the unit when you took the reading. Leave it out if your client has no such override: omitted means “not reported”, which is recorded differently from false. |
room_target |
float | No | The room temperature the schedule asked for in that slot (schedule.room_target_temps[slot], returned by GET /api/v1/schedules), as opposed to target_temp, the setpoint the unit was carrying. Send it only if your client steers the unit toward room_target_temps; otherwise leave it out. |
appliance_id |
string (UUID) | No | Tags the reading with a specific HVAC (or dehumidifier) unit. A reading without it is accepted but can’t be attributed to one unit, so send it on every reading once the account has more than one reading-producing appliance. When present it must be an HVAC or dehumidifier appliance the caller owns, else 400. |
Client identity
Section titled “Client identity”The batch-level client object names the client software. It never affects the schedules you get back, and leaving the whole block out changes nothing else.
| Field | Type | Required | Constraints / notes |
|---|---|---|---|
kind |
string | Yes (within client) |
"hacs" for a self-install integration, "node" for a managed fleet node, "custom" for any third-party client. Anything else is rejected with 422. |
version |
string | Yes (within client) |
Your client’s version string, 1–40 characters. |
node_bundle |
string | No | The node bundle tag, if you run one. null otherwise. |
ha_version |
string | No | Your Home Assistant version, if you have one. |
name |
string | No | A label for your client, 1–40 characters (letters, digits, space, ., _, -). Send it with kind: "custom" to say what your client is. |
Building your own client? Send {"kind": "custom", "name": "<your client>"}. Before this field existed, a third-party client had no honest value for kind and was told to omit the block entirely; "custom" is now that honest value.
Example (minimum viable reading)
Section titled “Example (minimum viable reading)”curl -X POST https://api.hungrymachines.io/api/v1/readings \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "readings": [ { "timestamp": "2025-11-18T14:30:00+00:00", "indoor_temp": 72.5, "hvac_state": "COOL" } ] }'Response (201)
Section titled “Response (201)”{ "accepted": 1}The accepted count reflects how many readings were stored.
Errors
Section titled “Errors”| Status | Detail | Cause |
|---|---|---|
| 400 | "appliance_id <uuid> is not a hvac or dehumidifier appliance owned by this user" |
appliance_id doesn’t reference an HVAC or dehumidifier unit the caller owns |
| 401 | "Not authenticated" |
No Authorization header |
| 401 | "Invalid credentials" |
The token is not valid. An expired token gets this string on this route, so refresh it and retry. |
| 422 | Array detail (see Errors & limits) |
A field failed validation: indoor_temp outside 30–120, an unrecognised hvac_state, zero or more than 100 readings, a non-string label, an indoor_source other than "sensor" / "entity", a client.kind that isn’t "hacs", "node" or "custom", a client.version that is empty or over 40 characters, or a client.name that’s empty, over 40 characters, or has a character other than letters/digits/space/./_/-. The whole batch is rejected. |
| 429 | "Rate limit exceeded: max <cap> readings per user per day (scaled to <N> active reading-producing appliance(s))" |
Over the daily reading ceiling (see below) |
| 503 | "Database unavailable" |
Temporary; retry |
Rate limit
Section titled “Rate limit”The ceiling is 600 readings per active reading-producing appliance (HVAC or dehumidifier), per account, per UTC day. An account with no such appliance registered yet gets 600. One unit reporting every 5 minutes sends 288 a day, so the limit only trips on a stuck loop or a very large catch-up batch.
How readings are used
Section titled “How readings are used”Hungry Machines uses your readings to learn how your home heats and cools and to generate the optimized schedules you pull from /api/v1/schedule. Schedules improve as readings accumulate, so keep pushing consistently; gaps slow that down, because there’s less to learn from. A stored schedule does not wait for readings: it appears with the first planning run after the appliance is registered (see Schedule).