Other endpoints
Find BACnet devices (Who-Is / I-Am)
Broadcasts Who-Is globally (*:*) by default: every BACnet router
forwards it to the networks behind it (MS/TP trunks included), and
when Windrow is a foreign device (--foreign /
WINDROW_BACNET_FOREIGN_BBMD) its BBMD distributes it to the other
subnets. A device behind a router answers from a routed net:mac
address (e.g. 5:12), which is what a connection to it uses.
Pass network to broadcast on one remote network only,
global_broadcast: false to stay on this subnet, or address to
target one specific device (a directed Who-Is) - useful for testing
on a single host, since a locally-originated broadcast doesn't loop
back to another process on the same machine over a real physical
interface (a macOS/BSD networking behavior, not something this
endpoint works around).
Find BACnet devices (Who-Is / I-Am) › Request Body optional
low_limitLower bound of the device-instance range to scan. Requires high_limit.
high_limitUpper bound of the device-instance range to scan. Requires low_limit.
addressTarget one specific device (directed Who-Is) instead of broadcasting - a local ip[:port] or a routed net:mac.
networkBroadcast on this one remote network (net:*) instead of globally.
global_broadcastfalse keeps the Who-Is to this subnet (a local broadcast), as before Baler#126.
timeoutSeconds to wait for I-Am responses. Defaults to bacpypes3's own Who-Is timeout (3s) if omitted.
Find BACnet devices (Who-Is / I-Am) › Responses
Every device that responded, as a JSON array (possibly empty).
device_instanceaddressThe address the device responded from, as every other call takes it: a BACnet/IP address (no port when it's the standard 47808), or for a device behind a router net:mac - its network number and its MAC on that network (5:12 for MS/TP MAC 12 on network 5, 7:10.0.1.20 for a BACnet/IP device on a routed IP network).
vendor_idnetworkThe device's network number when it's behind a router; null on this subnet.
macThe device's MAC on its network (the IP address on this subnet).
max_apduLargest APDU the device accepts, from its I-Am (480 is typical of MS/TP).
Find BACnet routers (Who-Is-Router-To-Network)
Asks every router reachable from here (through the BBMD too, as a foreign device) which networks it reaches. The network numbers are what POST /discover's network and a net:mac device address refer to. The routes learned are also cached (see GET /bacnet/network).
Find BACnet routers (Who-Is-Router-To-Network) › Request Body optional
networkOnly routers to this network.
timeoutFind BACnet routers (Who-Is-Router-To-Network) › Responses
Each router that answered and the networks it reaches.
addressnetworksHow this instance is attached to BACnet - BBMD / foreign-device state and known routes
mode is normal, foreign (registered with a BBMD: set with --foreign/--ttl or WINDROW_BACNET_FOREIGN_BBMD/WINDROW_BACNET_FOREIGN_TTL; the registration renews itself at the TTL) or bbmd (--bbmd or WINDROW_BACNET_BBMD_BDT). registration is the BBMD's answer to the last registration: registered, registering (no answer yet), unregistered, or rejected (code). routes are the routers learned so far, by network.
How this instance is attached to BACnet - BBMD / foreign-device state and known routes › Responses
The current state.
modebbmdttlregistrationbdtnetworksCommand a point (BACnet WriteProperty with priority, or a Modbus register/coil write)
Writes present-value of the point that publishes topic, at a BACnet priority (1-16, or 17 = relinquish-default); value: null relinquishes that priority. Only commandable objects (analog/binary/multi-state output and value) are accepted. The read-back value is published to MQTT immediately. Same handler as the command channel's point_write action.
If a Modbus connection publishes topic, the point must be writable (a holding register or coil). The value is in engineering units and is encoded with the point's type, order, scale and offset (rounded to the nearest step the register holds). Modbus devices have no priority array, so Windrow keeps one per point (the connection's write_levels) and writes the highest-priority level's value; relinquishing every level leaves the register at its last value.
Command a point (BACnet WriteProperty with priority, or a Modbus register/coil write) › Request Body
topicpriorityvalueNumber, boolean/active/inactive, state number, or null to relinquish.
expires_atUnix seconds; refused if already past.
Command a point (BACnet WriteProperty with priority, or a Modbus register/coil write) › Responses
Written; includes the read-back present value. present_value is null when the write succeeded but the read-back didn't (the next poll reports it).
topicpriorityvaluepresent_valueeffective_levelModbus only - the level whose value is now on the device, or null if every level is relinquished.
Read a device's Schedule and Calendar objects (as-built schedule check)
Reads every schedule and calendar object in the device's object-list now and returns them as JSON (Baler#165). An exception-schedule too big for one answer is read element by element. Same as the command channel's read_schedules; GET /connections/{id}/schedules does the same for a connection's device and names its points.
Read a device's Schedule and Calendar objects (as-built schedule check) › Request Body
device_addressdevice_instanceRead a device's Schedule and Calendar objects (as-built schedule check) › Responses
The device's schedules and calendars.
Enumerate a device's points
Reads the device's object list, then batch-reads
object-name/present-value/units per object. Every object comes
back, points included - a null present_value (e.g. on a
network-port object) is a hint it's not a sensor/actuator point,
not an error. units: null isn't a bug either: some objects
genuinely have no units property, and one missing property
never hides the rest of a point's fields or the other points on
the device.
Enumerate a device's points › Request Body
device_addressdevice_instanceEnumerate a device's points › Responses
Every object on the device, as a JSON array.
object_identifierThe real BACnet object identifier - what create-a-connection needs, not a display name.
labelobject-name if the device reports one, otherwise falls back to the object_identifier itself.
Current value at learn time - a snapshot, not a live feed. null if the object has none (e.g. a network-port) or the property read failed.
unitsList every active connection
List every active connection › Responses
Every connection currently persisted and polling, as a JSON array.
iddevice_addressip[:port] on this subnet, or net:mac for a device behind a router (5:12) - as /discover reports it. A broadcast address is a 400.
device_instancetopic_prefixMQTT topic prefix every point on this connection publishes under, as {topic_prefix}/{label}.
Optional - the login this device's own UI/gateway needs, if any. Write-only on create; a created/listed Connection always shows this redacted (see RedactedCredential).
Listings only: whether this connection is actually reading, since the process started. Kept in memory and never stored on the record; it resets when the connection is updated. Times are unix seconds.
device_nameName of the device this connection polls - what it announces itself as (windrow.announce), or a DNS host name. When set, Windrow re-points the connection's address when the device announces (or resolves to) a new one. Empty clears it.
poll_intervalSeconds between poll cycles.
read_multipleRead with ReadPropertyMultiple. Reads are batched per device, across every connection on it: points on one device tick together and go out as one request per read_multiple_max objects. A device that rejects RPM is read with one ReadProperty per point from then on; one whose answer doesn't fit an APDU gets smaller batches. false keeps one ReadProperty per point for this connection.
read_multiple_maxMost objects per ReadPropertyMultiple to this device. Default: what fits the device's max APDU (from its I-Am), at most WINDROW_RPM_MAX_POINTS (50). With several connections on one device the smallest value wins.
subscribe_covSubscribeCOV for every point (a point's own subscribe_cov overrides it), so a change publishes as soon as the device notifies. While a point's subscription is live it is still polled, every cov_poll_interval, in case a notification is lost; if the subscription fails it's polled at its normal interval and re-subscribed with back-off, and an object the device won't do COV for is just polled.
cov_lifetimeSeconds each COV subscription asks the device for. Renewed at half that.
cov_poll_intervalSeconds between the safety-net polls of a point whose COV subscription is live.
forward_alarmsForward the device's intrinsic-reporting alarms (Baler#164). Windrow adds itself to the recipient-list of every notification-class object on the device (this changes the controller's configuration, hence opt-in), re-checks that every WINDROW_ALARM_REFRESH seconds (default 3600), and reads the active alarms at start. Each event notification is published retained on {topic_prefix}/{label}/alarm for a point of this connection, or {topic_prefix}/alarms/{type}-{instance} for another object, as the usual {point, value, ts} envelope whose value is the event (see AlarmEvent). Turning it off or deleting the connection removes Windrow's recipient entries again.
backfill_windowMost seconds of history a trend-log backfill reads back for this connection's points with trend_log; 0 turns backfill off.
Create a connection and start polling it immediately
Pick whichever points /learn returned that you actually want
bridged. label is just the MQTT topic segment (matches every
other bridge's "point is whatever label makes sense" convention)
- rename it freely, it doesn't have to match what
/learnsuggested.
Create a connection and start polling it immediately › Request Body
device_addressip[:port] on this subnet, or net:mac for a device behind a router (5:12) - as /discover reports it. A broadcast address is a 400.
device_instancetopic_prefixMQTT topic prefix every point on this connection publishes under, as {topic_prefix}/{label}.
device_nameName of the device this connection polls - what it announces itself as (windrow.announce), or a DNS host name. When set, Windrow re-points the connection's address when the device announces (or resolves to) a new one. Empty clears it.
poll_intervalSeconds between poll cycles.
read_multipleRead with ReadPropertyMultiple. Reads are batched per device, across every connection on it: points on one device tick together and go out as one request per read_multiple_max objects. A device that rejects RPM is read with one ReadProperty per point from then on; one whose answer doesn't fit an APDU gets smaller batches. false keeps one ReadProperty per point for this connection.
read_multiple_maxMost objects per ReadPropertyMultiple to this device. Default: what fits the device's max APDU (from its I-Am), at most WINDROW_RPM_MAX_POINTS (50). With several connections on one device the smallest value wins.
subscribe_covSubscribeCOV for every point (a point's own subscribe_cov overrides it), so a change publishes as soon as the device notifies. While a point's subscription is live it is still polled, every cov_poll_interval, in case a notification is lost; if the subscription fails it's polled at its normal interval and re-subscribed with back-off, and an object the device won't do COV for is just polled.
cov_lifetimeSeconds each COV subscription asks the device for. Renewed at half that.
cov_poll_intervalSeconds between the safety-net polls of a point whose COV subscription is live.
forward_alarmsForward the device's intrinsic-reporting alarms (Baler#164). Windrow adds itself to the recipient-list of every notification-class object on the device (this changes the controller's configuration, hence opt-in), re-checks that every WINDROW_ALARM_REFRESH seconds (default 3600), and reads the active alarms at start. Each event notification is published retained on {topic_prefix}/{label}/alarm for a point of this connection, or {topic_prefix}/alarms/{type}-{instance} for another object, as the usual {point, value, ts} envelope whose value is the event (see AlarmEvent). Turning it off or deleting the connection removes Windrow's recipient entries again.
backfill_windowMost seconds of history a trend-log backfill reads back for this connection's points with trend_log; 0 turns backfill off.
Optional - the login this device's own UI/gateway needs, if any. Write-only on create; a created/listed Connection always shows this redacted (see RedactedCredential).
Create a connection and start polling it immediately › Responses
Created. Polling has already started - publishes land on {topic_prefix}/{label} per point, on the frozen {point, value, ts} envelope every Windrow bridge uses.
iddevice_addressip[:port] on this subnet, or net:mac for a device behind a router (5:12) - as /discover reports it. A broadcast address is a 400.
device_instancetopic_prefixMQTT topic prefix every point on this connection publishes under, as {topic_prefix}/{label}.
Optional - the login this device's own UI/gateway needs, if any. Write-only on create; a created/listed Connection always shows this redacted (see RedactedCredential).
Listings only: whether this connection is actually reading, since the process started. Kept in memory and never stored on the record; it resets when the connection is updated. Times are unix seconds.
device_nameName of the device this connection polls - what it announces itself as (windrow.announce), or a DNS host name. When set, Windrow re-points the connection's address when the device announces (or resolves to) a new one. Empty clears it.
poll_intervalSeconds between poll cycles.
read_multipleRead with ReadPropertyMultiple. Reads are batched per device, across every connection on it: points on one device tick together and go out as one request per read_multiple_max objects. A device that rejects RPM is read with one ReadProperty per point from then on; one whose answer doesn't fit an APDU gets smaller batches. false keeps one ReadProperty per point for this connection.
read_multiple_maxMost objects per ReadPropertyMultiple to this device. Default: what fits the device's max APDU (from its I-Am), at most WINDROW_RPM_MAX_POINTS (50). With several connections on one device the smallest value wins.
subscribe_covSubscribeCOV for every point (a point's own subscribe_cov overrides it), so a change publishes as soon as the device notifies. While a point's subscription is live it is still polled, every cov_poll_interval, in case a notification is lost; if the subscription fails it's polled at its normal interval and re-subscribed with back-off, and an object the device won't do COV for is just polled.
cov_lifetimeSeconds each COV subscription asks the device for. Renewed at half that.
cov_poll_intervalSeconds between the safety-net polls of a point whose COV subscription is live.
forward_alarmsForward the device's intrinsic-reporting alarms (Baler#164). Windrow adds itself to the recipient-list of every notification-class object on the device (this changes the controller's configuration, hence opt-in), re-checks that every WINDROW_ALARM_REFRESH seconds (default 3600), and reads the active alarms at start. Each event notification is published retained on {topic_prefix}/{label}/alarm for a point of this connection, or {topic_prefix}/alarms/{type}-{instance} for another object, as the usual {point, value, ts} envelope whose value is the event (see AlarmEvent). Turning it off or deleting the connection removes Windrow's recipient entries again.
backfill_windowMost seconds of history a trend-log backfill reads back for this connection's points with trend_log; 0 turns backfill off.
Edit a connection's points, poll interval, topic prefix or read options
Send any subset of points, poll_interval, topic_prefix and the read options (read_multiple, read_multiple_max, subscribe_cov, cov_lifetime, cov_poll_interval); anything else is a 400 (a typo'd field is refused, not ignored). points replaces the whole list, and each point keeps only the fields PointRef documents (so /learn output can be sent as-is; its present_value snapshot is dropped). The connection's poll tasks restart with the new settings, every point's tags and the {topic_prefix}/equip/tags record are republished, and each point that no longer publishes where it did - a removed label, or every point when topic_prefix changes - gets an empty retained message on {old_prefix}/{label} and {old_prefix}/{label}/tags (plus {old_prefix}/equip/tags on a prefix change), so its last value and tags stop being handed to new subscribers. The id is kept.
path Parameters
idEdit a connection's points, poll interval, topic prefix or read options › Request Body
device_nameName of the device this connection polls - what it announces itself as (windrow.announce), or a DNS host name. When set, Windrow re-points the connection's address when the device announces (or resolves to) a new one. Empty clears it.
device_addressRe-point the connection (also done automatically for a named device that moves).
Replaces every point. Labels must be unique.
poll_intervaltopic_prefixNo +/#; a trailing / is dropped.
read_multipleRead with ReadPropertyMultiple. Reads are batched per device, across every connection on it: points on one device tick together and go out as one request per read_multiple_max objects. A device that rejects RPM is read with one ReadProperty per point from then on; one whose answer doesn't fit an APDU gets smaller batches. false keeps one ReadProperty per point for this connection.
read_multiple_maxMost objects per ReadPropertyMultiple to this device. Default: what fits the device's max APDU (from its I-Am), at most WINDROW_RPM_MAX_POINTS (50). With several connections on one device the smallest value wins.
subscribe_covSubscribeCOV for every point (a point's own subscribe_cov overrides it), so a change publishes as soon as the device notifies. While a point's subscription is live it is still polled, every cov_poll_interval, in case a notification is lost; if the subscription fails it's polled at its normal interval and re-subscribed with back-off, and an object the device won't do COV for is just polled.
cov_lifetimeSeconds each COV subscription asks the device for. Renewed at half that.
cov_poll_intervalSeconds between the safety-net polls of a point whose COV subscription is live.
forward_alarmsForward the device's intrinsic-reporting alarms (Baler#164). Windrow adds itself to the recipient-list of every notification-class object on the device (this changes the controller's configuration, hence opt-in), re-checks that every WINDROW_ALARM_REFRESH seconds (default 3600), and reads the active alarms at start. Each event notification is published retained on {topic_prefix}/{label}/alarm for a point of this connection, or {topic_prefix}/alarms/{type}-{instance} for another object, as the usual {point, value, ts} envelope whose value is the event (see AlarmEvent). Turning it off or deleting the connection removes Windrow's recipient entries again.
backfill_windowMost seconds of history a trend-log backfill reads back for this connection's points with trend_log; 0 turns backfill off.
Edit a connection's points, poll interval, topic prefix or read options › Responses
The updated connection, credential redacted. Polling has already restarted.
iddevice_addressip[:port] on this subnet, or net:mac for a device behind a router (5:12) - as /discover reports it. A broadcast address is a 400.
device_instancetopic_prefixMQTT topic prefix every point on this connection publishes under, as {topic_prefix}/{label}.
Optional - the login this device's own UI/gateway needs, if any. Write-only on create; a created/listed Connection always shows this redacted (see RedactedCredential).
Listings only: whether this connection is actually reading, since the process started. Kept in memory and never stored on the record; it resets when the connection is updated. Times are unix seconds.
device_nameName of the device this connection polls - what it announces itself as (windrow.announce), or a DNS host name. When set, Windrow re-points the connection's address when the device announces (or resolves to) a new one. Empty clears it.
poll_intervalSeconds between poll cycles.
read_multipleRead with ReadPropertyMultiple. Reads are batched per device, across every connection on it: points on one device tick together and go out as one request per read_multiple_max objects. A device that rejects RPM is read with one ReadProperty per point from then on; one whose answer doesn't fit an APDU gets smaller batches. false keeps one ReadProperty per point for this connection.
read_multiple_maxMost objects per ReadPropertyMultiple to this device. Default: what fits the device's max APDU (from its I-Am), at most WINDROW_RPM_MAX_POINTS (50). With several connections on one device the smallest value wins.
subscribe_covSubscribeCOV for every point (a point's own subscribe_cov overrides it), so a change publishes as soon as the device notifies. While a point's subscription is live it is still polled, every cov_poll_interval, in case a notification is lost; if the subscription fails it's polled at its normal interval and re-subscribed with back-off, and an object the device won't do COV for is just polled.
cov_lifetimeSeconds each COV subscription asks the device for. Renewed at half that.
cov_poll_intervalSeconds between the safety-net polls of a point whose COV subscription is live.
forward_alarmsForward the device's intrinsic-reporting alarms (Baler#164). Windrow adds itself to the recipient-list of every notification-class object on the device (this changes the controller's configuration, hence opt-in), re-checks that every WINDROW_ALARM_REFRESH seconds (default 3600), and reads the active alarms at start. Each event notification is published retained on {topic_prefix}/{label}/alarm for a point of this connection, or {topic_prefix}/alarms/{type}-{instance} for another object, as the usual {point, value, ts} envelope whose value is the event (see AlarmEvent). Turning it off or deleting the connection removes Windrow's recipient entries again.
backfill_windowMost seconds of history a trend-log backfill reads back for this connection's points with trend_log; 0 turns backfill off.
The connection's device's active alarms, read now (GetEventInformation)
path Parameters
idThe connection's device's active alarms, read now (GetEventInformation) › Responses
Every object on the device not in a normal event state, or with a transition not yet acknowledged.
objectevent_statenotify_typetimestampsThe connection's device's schedules, with its points named in `controls`
Set (or replace) the login this connection's device needs
Neither BACnet nor Modbus has protocol-level auth - this is purely bookkeeping for whatever manual login the physical device or its gateway needs (e.g. its own local web UI). Stored encrypted at rest. The response shows the plaintext password once, at set time only - every later GET /connections redacts it to null, same pattern as /webhooks's secret in baler's fault API.
path Parameters
idSet (or replace) the login this connection's device needs › Request Body
usernamepasswordStored encrypted at rest (Fernet, keyed by the WINDROW_CREDENTIAL_KEY env var) - never returned in plaintext again after this call's response.
Set (or replace) the login this connection's device needs › Responses
Set. Password shown once, here only.
usernamepasswordShown once, here only - every later GET of the owning record redacts this to null.
Sweep a subnet for Modbus/TCP devices
Modbus has no discovery protocol - this TCP-connect-scans every host in cidr on port (bounded to concurrency simultaneous attempts) and, where the device implements it, best-effort reads its Basic Device Identification (Modbus function 0x2B/0x0E) for vendor/product/revision. A device that doesn't implement 0x2B/0x0E is still a real find - it comes back with those three fields null, not omitted from the results or treated as an error.
Sweep a subnet for Modbus/TCP devices › Request Body
cidrSubnet to sweep, in CIDR notation. No default - Modbus periodic/on-demand discovery only ever scans a subnet the operator explicitly supplied.
portconcurrencyMax simultaneous TCP-connect attempts, bounded so a large sweep doesn't exhaust local ephemeral ports/file descriptors.
timeoutSeconds to wait for each host's TCP connect.
Sweep a subnet for Modbus/TCP devices › Responses
Every host that accepted a Modbus TCP connection, as a JSON array (possibly empty).
hostportvendorFrom Modbus function 0x2B/0x0E (Read Device Identification) - null if the device doesn't implement it, which is common and not an error.
product_coderevisionRead one holding register from a device before connecting it
Opens a fresh connection, reads register once and closes it again. Nothing is stored or published. A host that doesn't answer, or an exception code from the device, is a 400 with what went wrong.
Read one holding register from a device before connecting it › Request Body
hostregisterportdevice_idModbus unit id.
Optional - a point in its stored object form (type, function, byte/word order...), as in a connection's points. Without it, register is read as one unsigned holding register.
Read one holding register from a device before connecting it › Responses
The register's value.
hostportdevice_idregistervalueThe decoded value (a number or boolean).
List every active Modbus connection
List every active Modbus connection › Responses
Every Modbus connection currently persisted and polling, as a JSON array.
idhosttopic_prefixMQTT topic prefix every point on this connection publishes under, as {topic_prefix}/{label}.
{label: point} - Modbus has no self-describing object list, so these are supplied directly from the vendor's register map. Each point is either a bare register address (the original form: one holding register read as an unsigned 16-bit integer) or a ModbusPoint object naming its function code, data type, word/byte order, scale/offset, unit and writability. Both forms can be mixed in one connection.
null if no credential has been set for this device/connection.
Listings only: whether this connection is actually reading, since the process started. Kept in memory and never stored on the record; it resets when the connection is updated. Times are unix seconds.
Read-only. The priority levels commanded on each writable point through POST /write, as {label: {level: value}} (levels as strings). Modbus devices have no priority array, so Windrow keeps this one and writes the highest level's value. Absent when nothing is commanded.
device_nameName of the device this connection polls - what it announces itself as (windrow.announce), or a DNS host name. When set, Windrow re-points the connection's address when the device announces (or resolves to) a new one. Empty clears it.
portdevice_idModbus unit/slave id.
poll_intervalSeconds between poll cycles - one shared interval for every point on this connection (unlike BACnet connections, Modbus connections don't support a per-point poll_interval).
Optional Haystack unit symbol per point label (a register is just a number). Published as each point's unit tag on {topic_prefix}/{label}/tags. A point object can carry its own unit instead; naming one point in both is a 400.
Create a Modbus connection and start polling it immediately
Modbus has no self-describing object list the way BACnet does - register addresses are opaque and vendor-documented, so points is a {label: point} map you supply directly, not something a /learn-equivalent suggests - each point a bare holding-register address or a ModbusPoint object (function code, data type, word/byte order, scale/offset, unit, writable). Like a BACnet connection, it also publishes (retained) each point's Haystack tags on {topic_prefix}/{label}/tags - markers from the label's words, plus unit from units - and one {topic_prefix}/equip/tags naming its points, so Baler gives the connection an equipment record.
Create a Modbus connection and start polling it immediately › Request Body
hosttopic_prefixMQTT topic prefix every point on this connection publishes under, as {topic_prefix}/{label}.
{label: point} - Modbus has no self-describing object list, so these are supplied directly from the vendor's register map. Each point is either a bare register address (the original form: one holding register read as an unsigned 16-bit integer) or a ModbusPoint object naming its function code, data type, word/byte order, scale/offset, unit and writability. Both forms can be mixed in one connection.
device_nameName of the device this connection polls - what it announces itself as (windrow.announce), or a DNS host name. When set, Windrow re-points the connection's address when the device announces (or resolves to) a new one. Empty clears it.
portdevice_idModbus unit/slave id.
poll_intervalSeconds between poll cycles - one shared interval for every point on this connection (unlike BACnet connections, Modbus connections don't support a per-point poll_interval).
Optional Haystack unit symbol per point label (a register is just a number). Published as each point's unit tag on {topic_prefix}/{label}/tags. A point object can carry its own unit instead; naming one point in both is a 400.
Create a Modbus connection and start polling it immediately › Responses
Created. Polling has already started - publishes land on {topic_prefix}/{label} per point, on the frozen {point, value, ts} envelope every Windrow bridge uses.
idhosttopic_prefixMQTT topic prefix every point on this connection publishes under, as {topic_prefix}/{label}.
{label: point} - Modbus has no self-describing object list, so these are supplied directly from the vendor's register map. Each point is either a bare register address (the original form: one holding register read as an unsigned 16-bit integer) or a ModbusPoint object naming its function code, data type, word/byte order, scale/offset, unit and writability. Both forms can be mixed in one connection.
null if no credential has been set for this device/connection.
Listings only: whether this connection is actually reading, since the process started. Kept in memory and never stored on the record; it resets when the connection is updated. Times are unix seconds.
Read-only. The priority levels commanded on each writable point through POST /write, as {label: {level: value}} (levels as strings). Modbus devices have no priority array, so Windrow keeps this one and writes the highest level's value. Absent when nothing is commanded.
device_nameName of the device this connection polls - what it announces itself as (windrow.announce), or a DNS host name. When set, Windrow re-points the connection's address when the device announces (or resolves to) a new one. Empty clears it.
portdevice_idModbus unit/slave id.
poll_intervalSeconds between poll cycles - one shared interval for every point on this connection (unlike BACnet connections, Modbus connections don't support a per-point poll_interval).
Optional Haystack unit symbol per point label (a register is just a number). Published as each point's unit tag on {topic_prefix}/{label}/tags. A point object can carry its own unit instead; naming one point in both is a 400.
Edit a Modbus connection
Same shape and reasoning as PATCH /connections/{id}, with any subset of points (replaces the whole {label: point} map), units, poll_interval, topic_prefix, host, port and device_id. The poll task and its Modbus client are closed and reopened, so a new host/port takes effect at once. Modbus connections publish no tags of their own, but {old_prefix}/{label}/tags is cleared along with the value topic for each removed point anyway, in case a human tagged it.
path Parameters
idEdit a Modbus connection › Request Body
device_nameName of the device this connection polls - what it announces itself as (windrow.announce), or a DNS host name. When set, Windrow re-points the connection's address when the device announces (or resolves to) a new one. Empty clears it.
{label: point} - Modbus has no self-describing object list, so these are supplied directly from the vendor's register map. Each point is either a bare register address (the original form: one holding register read as an unsigned 16-bit integer) or a ModbusPoint object naming its function code, data type, word/byte order, scale/offset, unit and writability. Both forms can be mixed in one connection.
Replaces the {label: unit} map.
poll_intervaltopic_prefixhostportdevice_idEdit a Modbus connection › Responses
The updated connection, credential redacted. Polling has already restarted.
idhosttopic_prefixMQTT topic prefix every point on this connection publishes under, as {topic_prefix}/{label}.
{label: point} - Modbus has no self-describing object list, so these are supplied directly from the vendor's register map. Each point is either a bare register address (the original form: one holding register read as an unsigned 16-bit integer) or a ModbusPoint object naming its function code, data type, word/byte order, scale/offset, unit and writability. Both forms can be mixed in one connection.
null if no credential has been set for this device/connection.
Listings only: whether this connection is actually reading, since the process started. Kept in memory and never stored on the record; it resets when the connection is updated. Times are unix seconds.
Read-only. The priority levels commanded on each writable point through POST /write, as {label: {level: value}} (levels as strings). Modbus devices have no priority array, so Windrow keeps this one and writes the highest level's value. Absent when nothing is commanded.
device_nameName of the device this connection polls - what it announces itself as (windrow.announce), or a DNS host name. When set, Windrow re-points the connection's address when the device announces (or resolves to) a new one. Empty clears it.
portdevice_idModbus unit/slave id.
poll_intervalSeconds between poll cycles - one shared interval for every point on this connection (unlike BACnet connections, Modbus connections don't support a per-point poll_interval).
Optional Haystack unit symbol per point label (a register is just a number). Published as each point's unit tag on {topic_prefix}/{label}/tags. A point object can carry its own unit instead; naming one point in both is a 400.
Set (or replace) the login this Modbus connection's device needs
Same shape and reasoning as PATCH /connections/{id}/credential.
path Parameters
idSet (or replace) the login this Modbus connection's device needs › Request Body
usernamepasswordStored encrypted at rest (Fernet, keyed by the WINDROW_CREDENTIAL_KEY env var) - never returned in plaintext again after this call's response.
Set (or replace) the login this Modbus connection's device needs › Responses
Set. Password shown once, here only.
usernamepasswordShown once, here only - every later GET of the owning record redacts this to null.
List every device either periodic scan has ever seen
Populated by the periodic BACnet Who-Is scan (always running, interval set by --bacnet-discovery-interval) and, if configured, one or more periodic Modbus CIDR sweeps (--modbus-scan-cidr, off by default - no safe subnet to guess). status is pending (seen, no connection provisioned yet), provisioned (a matching connection exists), or ignored (a human dismissed it - survives rescans). default_credential_flag is always null for BACnet devices (no HTTP-login concept applies) and best-effort for Modbus devices (see /devices/{id}/recheck-credentials).
List every device either periodic scan has ever seen › Responses
Every known device, as a JSON array, credentials redacted.
idprotocolfirst_seen_atUnix epoch seconds.
last_seen_atUnix epoch seconds.
statusdevice_instanceBACnet only.
addressBACnet only.
vendor_idBACnet only.
hostModbus only.
portModbus only.
vendorModbus only.
product_codeModbus only.
revisionModbus only.
device_idModbus only, and only on a device added by address (POST /devices) - a sweep can't learn the unit id.
connection_idSet once a matching connection exists for this device.
credentialnull if no credential has been set for this device/connection.
default_credential_flagSet if the best-effort default-credential check found one that worked. Always null for BACnet devices (no HTTP-login concept applies - see /devices's description).
default_credential_checked_atAdd a device by address
For a device a scan can't find - broadcasts filtered between subnets, or Modbus on a subnet no --modbus-scan-cidr covers. Creates the same record a scan would (keyed the same way, so a later scan that does hear the device updates this record rather than adding a second one), with status pending - or provisioned if a connection already polls that device. Adding a device that's already known returns its existing record with 200. A BACnet device_address on the standard port 47808 is stored without the port, the way discovery reports it; a non-standard port is kept. A new pending Modbus device gets the same one-time default-credential check a scanned one does.
Add a device by address › Request Body
Decision Table
| Variant | Matching Criteria |
|---|---|
| type = object · requires: protocol, device_address, device_instance | |
| type = object · requires: protocol, host |
protocoldevice_addressdevice_instanceAdd a device by address › Responses
Already known. The existing device record, unchanged, credentials redacted.
idprotocolfirst_seen_atUnix epoch seconds.
last_seen_atUnix epoch seconds.
statusdevice_instanceBACnet only.
addressBACnet only.
vendor_idBACnet only.
hostModbus only.
portModbus only.
vendorModbus only.
product_codeModbus only.
revisionModbus only.
device_idModbus only, and only on a device added by address (POST /devices) - a sweep can't learn the unit id.
connection_idSet once a matching connection exists for this device.
credentialnull if no credential has been set for this device/connection.
default_credential_flagSet if the best-effort default-credential check found one that worked. Always null for BACnet devices (no HTTP-login concept applies - see /devices's description).
default_credential_checked_atSet a device's status (e.g. dismiss it as "ignored")
path Parameters
idSet a device's status (e.g. dismiss it as "ignored") › Request Body
statusSet a device's status (e.g. dismiss it as "ignored") › Responses
Updated device record, credentials redacted.
idprotocolfirst_seen_atUnix epoch seconds.
last_seen_atUnix epoch seconds.
statusdevice_instanceBACnet only.
addressBACnet only.
vendor_idBACnet only.
hostModbus only.
portModbus only.
vendorModbus only.
product_codeModbus only.
revisionModbus only.
device_idModbus only, and only on a device added by address (POST /devices) - a sweep can't learn the unit id.
connection_idSet once a matching connection exists for this device.
credentialnull if no credential has been set for this device/connection.
default_credential_flagSet if the best-effort default-credential check found one that worked. Always null for BACnet devices (no HTTP-login concept applies - see /devices's description).
default_credential_checked_atSet (or replace) the login a discovered-but-not-yet-provisioned device needs
For a device that needs a credential recorded before any connection exists for it yet (e.g. a gateway's web login that will never be polled as a point). Same shape and reasoning as PATCH /connections/{id}/credential.
path Parameters
idSet (or replace) the login a discovered-but-not-yet-provisioned device needs › Request Body
usernamepasswordStored encrypted at rest (Fernet, keyed by the WINDROW_CREDENTIAL_KEY env var) - never returned in plaintext again after this call's response.
Set (or replace) the login a discovered-but-not-yet-provisioned device needs › Responses
Set. Password shown once, here only.
usernamepasswordShown once, here only - every later GET of the owning record redacts this to null.
Re-run the default-credential check against this device
Automatically run once, the first time a Modbus device is seen as pending (never for BACnet - see /devices's description). This lets a human re-trigger it later, e.g. after changing the device's password, to confirm default_credential_flag clears. A short hardcoded list of common defaults (admin/admin, etc.) tried as HTTP Basic Auth against the device's own address, GET only, non-destructive.
path Parameters
idRe-run the default-credential check against this device › Responses
Updated device record, credentials redacted.
idprotocolfirst_seen_atUnix epoch seconds.
last_seen_atUnix epoch seconds.
statusdevice_instanceBACnet only.
addressBACnet only.
vendor_idBACnet only.
hostModbus only.
portModbus only.
vendorModbus only.
product_codeModbus only.
revisionModbus only.
device_idModbus only, and only on a device added by address (POST /devices) - a sweep can't learn the unit id.
connection_idSet once a matching connection exists for this device.
credentialnull if no credential has been set for this device/connection.
default_credential_flagSet if the best-effort default-credential check found one that worked. Always null for BACnet devices (no HTTP-login concept applies - see /devices's description).
default_credential_checked_atRecent jobs, without their results
Recent jobs, without their results › Responses
Jobs, oldest first.
idjobstatestarted_atunix seconds
finished_atmodbus_discover only - hosts probed so far.
resultWhen done - what the synchronous route returns. Left out of the listing.
errorStart a slow action (discover, learn, Modbus sweep) in the background
Answers at once with a job; poll GET /jobs/{id} for progress and, once state is done, the result (what the synchronous route would have returned). The synchronous routes give up after 15 s, so a /24 sweep or a big learn should use this. A finished discover or modbus_discover also records what it found as devices, like the periodic scans. A second start of a discover or sweep while one is running returns the running job. Jobs are in memory only: the newest 20 are kept, none across a restart.
Start a slow action (discover, learn, Modbus sweep) in the background › Request Body
jobStart a slow action (discover, learn, Modbus sweep) in the background › Responses
Started (or the same kind already running).
idjobstatestarted_atunix seconds
finished_atmodbus_discover only - hosts probed so far.
resultWhen done - what the synchronous route returns. Left out of the listing.
errorOne job's state, progress and (when done) result
path Parameters
idOne job's state, progress and (when done) result › Responses
The job.
idjobstatestarted_atunix seconds
finished_atmodbus_discover only - hosts probed so far.
resultWhen done - what the synchronous route returns. Left out of the listing.
errorThis node's status (the heartbeat's payload, over HTTP)
This node's status (the heartbeat's payload, over HTTP) › Responses
Same fields as the retained cmd/
announcedDevices that have announced themselves (windrow.announce), ever.
announced_onlineOf those, how many announced within the last 3 intervals.
announce_rejectedAnnouncements refused since start (outside --announce-allow, bad signature, malformed).
onlinesite_idinstance_idversionstarted_attsbacnet_connectionsbacnet_networkBACnet mode and, for a foreign device, its BBMD registration (see /bacnet/network).
bacnet_requestsBACnet requests sent since startup by kind
modbus_connectionsdevicespending_devicesnetworksSubnets this node sits on (its BACnet --address and any --modbus-scan-cidr), as CIDRs.
Devices that announced themselves ("I am here")
Filled by python -m windrow.announce senders on the LAN, when this Windrow listens (--announce-port). A device's address is the datagram's source IP. online is false after 3 missed intervals. history lists address changes; connections whose device_name matches were re-pointed.
Devices that announced themselves ("I am here") › Responses
Every device ever announced.
nameaddressfqdnonlinelast_seenfirst_seen
