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 (Bearer JWT)
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 | Which software is pushing. Display-only — 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". Common aliases ("cooling", "eco_cool", "dehumidify", "fan_only", "heat_cool", "auto", "idle") are normalized server-side — pass whatever your thermostat reports. "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 — it tells us what the comfort band was measured against. |
override_active | boolean | No | true if your own local comfort guard, rather than our schedule, was commanding the unit when you took the reading. Leave it out if you don’t have one: omitted means “not reported”, which we treat differently from false. |
room_target | float | No | The room temperature our schedule asked you to hold at that moment (schedule.room_target_temps[slot]), as opposed to target_temp, the setpoint the unit was carrying. Send it if you drive the unit toward a room target rather than commanding the setpoint once — it is how we tell a working tracking loop from a mis-commanded unit. Leave it out if you don’t. |
appliance_id | string (UUID) | Conditional | Tags the reading with a specific HVAC (or dehumidifier) unit. Required once the account has more than one reading-producing appliance; optional for a single-HVAC account. Must reference an appliance the caller owns. |
Client identity
Section titled “Client identity”Alongside readings you may send one batch-level client object naming the software on your end. It is display-only: we use it to tell which release a home’s data was produced under when we grade a control change, and it never affects the schedules you get back. Leave it out and nothing else changes.
| Field | Type | Required | Constraints / notes |
|---|---|---|---|
kind | string | Yes (within client) | "hacs" for a self-install integration, "node" for a managed fleet node. Anything else is rejected with 422. |
version | string | Yes (within client) | Your integration’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. |
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 | "indoor_temp must be between 30 and 120" | A field failed validation |
| 400 | "appliance_id <uuid> is not a hvac appliance owned by this user" | appliance_id doesn’t reference an HVAC (or dehumidifier) unit the caller owns |
| 401 | "Not authenticated" | Missing or invalid token |
| 422 | Validation error on client | client.kind isn’t "hacs" or "node", or client.version is empty or over 40 characters |
| 429 | "Rate limit exceeded: ..." | Over the daily reading ceiling (see below) |
Rate limit
Section titled “Rate limit”The daily reading ceiling is a runaway-loop safeguard, not a normal-use limit. It scales with how many appliances the account runs — at one reading every 5 minutes, a single HVAC generates ~288 readings/day, comfortably under the cap. It only trips on stuck loops or very large catch-up batches. If you legitimately run several appliances, tag each reading with its appliance_id — the ceiling scales with your active appliance count.
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. It needs a few days of continuous readings before it switches from safe defaults to optimized schedules — keep pushing consistently and the switch happens on its own. Gaps slow it down, because there’s less to learn from.