Skip to content

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.

Auth: Required (Bearer JWT)

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.

{
"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"
}
}
FieldTypeRequiredConstraints / notes
readingsarrayYes1–100 reading objects (below)
clientobjectNoWhich software is pushing. Display-only — see Client identity.
FieldTypeRequiredConstraints / notes
timestampstring (ISO 8601)YesUTC recommended
indoor_tempfloatYes30.0–120.0 °F
hvac_statestringYes"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_humidityfloatNoPercent (0–100)
outdoor_tempfloatNoFahrenheit
target_tempfloatNoCurrent thermostat setpoint
fan_modestringNoFan-speed label, e.g. "auto", "low", "high". Stored as sent.
power_wattsfloatNoCurrent power draw. Send only if the meter measures this specific unit — a shared/whole-home meter can mislead the model.
hvac_actionstringNoThe 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_modestringNoThe mode your integration last commanded ("COOL"/"HEAT"/"OFF"). Send when available; improves accuracy.
commanded_fan_modestringNoThe fan label your integration last commanded.
commanded_setpointfloatNoThe setpoint (°F) your integration last commanded.
indoor_sourcestringNoWhich 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_activebooleanNotrue 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_targetfloatNoThe 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_idstring (UUID)ConditionalTags 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.

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.

FieldTypeRequiredConstraints / notes
kindstringYes (within client)"hacs" for a self-install integration, "node" for a managed fleet node. Anything else is rejected with 422.
versionstringYes (within client)Your integration’s version string, 1–40 characters.
node_bundlestringNoThe node bundle tag, if you run one. null otherwise.
ha_versionstringNoYour Home Assistant version, if you have one.
Terminal window
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"
}
]
}'
{
"accepted": 1
}

The accepted count reflects how many readings were stored.

StatusDetailCause
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
422Validation error on clientclient.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)

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.

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.