Haystack API
Baler serves the standard Project Haystack HTTP API,
so tools that already speak Haystack can use it without changes. SkySpark, Haxall's haystack
connector, nhaystack's client side, pyhaystack, phable and openhay's hay all work this way:
point the tool at Baler instead of a SkySpark host.
Code
<project> is accepted so SkySpark-shaped URLs work, but it doesn't scope anything yet.
Logging in
Use your normal Baler email and password. Haystack clients run the standard HELLO →
SCRAM-SHA-256 handshake. Your password is never sent: it's checked against the same password hash
POST /auth/login uses, so there's nothing to reset and no second password to manage. The
handshake ends with a normal session JWT as the Haystack authToken.
You can also skip SCRAM and send any token the gateway accepts (a session JWT or an API key from
POST /auth/api-keys) as either:
Code
Read-type ops (about, read, nav, hisRead, watch*) need the viewer role.
hisWrite needs operator. Login attempts are rate-limited per IP at the gateway.
Try it
With openhay's hay CLI:
Code
From Python (pip install -e ".[haystack]" installs openhay):
Code
What a record looks like
Records come from the same entity graph everything else reads:
| Haystack tag | where it comes from |
|---|---|
id | the entity URI. Characters Haystack ids don't allow are escaped as ~XX, e.g. @urn:point:windrow~2Fahu-1~2FanalogInput~2C1 |
markers (temp, sensor, …) | haystack:hasTag facts, as written by ingest and the Haystack puller |
other tags (dis, kind, …) | haystack:<tag> values |
point / equip | the kind of URI |
equipRef | brick:isPointOf in the graph, not the original source's ref, so it always resolves here |
brickClass | the Brick class mapping assigned |
spec | the most specific ph Xeto spec the record fits (@ph::Ahu, @ph.points::ZoneAirTempSensor): from its own markers plus those its Brick class implies, so Brick-only records are typed for Xeto clients too. A stored spec (Haystack 4 import) is kept as-is. |
curVal, unit, his, mod | the newest sample in the historian |
Filters work exactly like the Query page: a name matches as a Haystack marker,
a Brick class (Air_Handling_Unit) or a Brick tag word. So read ahu also finds AHUs that
never had a Haystack ahu tag. Tag paths (point and equipRef->ahu,
equipRef->siteRef->dis == "HQ"), units (area > 1000ft² matches only ft², with no
conversion), date, time and Uri literals, and ^symbol terms all work.
Who sees what
Reads are scoped to the caller's sites once sites own data. See Multi-Site. Anything out of scope reads as if it didn't exist.
Formats
Requests can be GET with Zinc-encoded query params, or POST with a grid in Zinc, JSON (Haystack 3
or Haystack 4/Hayson) or Trio. Responses follow Accept: Zinc by default, plus application/json,
text/trio and text/csv. A JSON request gets JSON back in the same flavor, so older Haystack 3
clients keep working.
Commanding points (pointWrite)
pointWrite with a level (1-17) commands a real BACnet point. A val of null relinquishes that
level. Level 17 is the default the point falls back to when every other level is empty (BACnet's
relinquish-default): it can be set but not relinquished. It works the way an operator's command at a controller does: a write at 8 overrides the
controller's own logic until it's released.
- Writable points: Windrow tags BACnet setpoints and commands (analog, binary and multi-state
output/value objects, never sensors) as
writable. Only those accept a write. - Routing: the write goes to the Windrow instance that owns the point. That instance must run
with
--site-id, which is how it advertises its command channel. The command travels over that site's broker-ACL-protected MQTT channel. - Confirmation: a successful response means the controller accepted the BACnet WriteProperty. Nothing is merely queued.
- Offline sites: if the site is offline, the call fails after 10 seconds. The command expires 30 seconds after it was sent, and the site refuses it after that, so it can never apply a stale setpoint when it reconnects.
- Timed overrides: a level-8 write with a
duration(30min,2h) is relinquished automatically when it runs out; without one it stays until released, as in Haxall. The array'sexpirescolumn shows the time left. - Reading the array:
pointWritewithout alevelreturns the 17-level array of what Baler has commanded (level,levelDis,val,valDis,who,expires, as Haxall returns it). The controller's own array can also hold other writers' levels. - Units: a Number's unit is not converted. Send the value in the point's own unit.
- Roles: writing needs the
operatorrole. Haystack clients POST pointWrite for reads too, so reading the array also needsoperator.
Deliberately unsupported
invokeActionanswers with a Haystack error grid (HTTP 200,errmeta) that says it isn't supported, andopsdoesn't list it. In SkySpark and Haxall an action is an Axon expression in a record'sactionsgrid. Baler records define no actions and Baler has no Axon to run one. To command a point, usepointWrite.- A Symbol as a comparison value (
kind == ^elec) is a filter parse error. Haystack's filter grammar doesn't include it. A bare^hot-waterterm works and meanshot and water.
Ops, checked against Haxall
hisRead takes every range the spec defines (today, yesterday, a date, a date range, two
dateTimes, or one dateTime meaning "until now"), and the batch form (meta range, one id row
per point, returning ts, v0..vN). hisWrite takes the batch form too (a ts column plus one
column per point, each naming its point with an id in the column meta). close ends the session:
the token is refused by /api at once and by the gateway, on every API, within 30 seconds. ops returns the
Haystack 4 op defs. Each of these is a column in the CI conformance matrix, run against Haxall
as the reference (Haystack conformance).

