bricklogger-ibos 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 CX1 ApS
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,398 @@
1
+ Metadata-Version: 2.4
2
+ Name: bricklogger-ibos
3
+ Version: 0.1.0
4
+ Summary: Bricklogger source: the trend history of BACnet objects from the iBOS Data API
5
+ Keywords: brick,bacnet,building-automation,ibos,bricklogger
6
+ Author: Michael Rosenlund
7
+ Author-email: Michael Rosenlund <michael.rosenlund@cx1.dk>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 2 - Pre-Alpha
11
+ Classifier: Intended Audience :: System Administrators
12
+ Classifier: Operating System :: POSIX :: Linux
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Topic :: Home Automation
18
+ Requires-Dist: bricklogger>=0.2,<0.3
19
+ Requires-Dist: httpx>=0.27
20
+ Requires-Dist: bacpypes3>=0.0.90
21
+ Requires-Python: >=3.11
22
+ Project-URL: Homepage, https://github.com/CX1-ApS/bricklogger-ibos
23
+ Project-URL: Source, https://github.com/CX1-ApS/bricklogger-ibos
24
+ Project-URL: Issues, https://github.com/CX1-ApS/bricklogger-ibos/issues
25
+ Description-Content-Type: text/markdown
26
+
27
+ # Bricklogger iBOS source
28
+
29
+ A source plugin for [Bricklogger](https://cx1-aps.github.io/bricklogger/) that collects the trend history of
30
+ BACnet objects from the **iBOS Data API**, the cloud service of iBOS
31
+ Technologies, for the points a Brick model gives an iBOS reference.
32
+
33
+ The API exposes the objects of a project's devices, with their trend history,
34
+ read-only over HTTPS, authenticated with a personal access token. It carries no
35
+ live values — what it serves is each object's trend history — so the source is
36
+ a **history source**: its collection method is `poll`, and every round fetches
37
+ the samples the cloud has received since the round before, each **with the
38
+ cloud's own timestamp**. The objects are the same objects a BACnet/IP source
39
+ would read on site, so a point may carry both a BACnet reference and an iBOS
40
+ reference; which source collects it follows from the source configuration, and
41
+ a point claimed by both is the validation error the
42
+ [architecture](https://cx1-aps.github.io/bricklogger/architecture/#which-source-owns-which-point) describes.
43
+
44
+ ## Install
45
+
46
+ The plugin is installed into Bricklogger's environment, from the CLI, the web
47
+ interface's Plugins screen or, in a container, into the plugin volume:
48
+
49
+ ```bash
50
+ bricklogger plugins add bricklogger-ibos
51
+ sudo systemctl restart bricklogger # or: bricklogger daemon restart
52
+ bricklogger plugins ibos # the settings and tools it brings
53
+ ```
54
+
55
+ | Plugin | Bricklogger |
56
+ |--------|-------------|
57
+ | 0.1 | 0.2 |
58
+
59
+ A plugin is built for one minor version of Bricklogger; after upgrading
60
+ Bricklogger across one, upgrade the plugin with the same `plugins add`.
61
+
62
+ Then put the token in Bricklogger's `env` file and add an instance — guided
63
+ with `bricklogger sources add ibos_main --type ibos`, or by hand:
64
+
65
+ ```
66
+ IBOS_PAT=...
67
+ ```
68
+
69
+ **Coming from Bricklogger 0.1,** where the iBOS source was built in: the
70
+ configuration, the type name `ibos`, the vocabulary and the collected history
71
+ are unchanged. After the upgrade to Bricklogger 0.2 the instance shows as
72
+ failed until this plugin is added and the daemon restarted; it then resumes
73
+ from the latest observation of every point.
74
+
75
+ ## Configuration
76
+
77
+ ```yaml
78
+ # sources.yaml
79
+ ibos_main:
80
+ type: ibos
81
+ token: ${IBOS_PAT} # required: personal access token
82
+ url: https://api.data.ibostechnologies.com # default
83
+ projects: [4711, "0d3f5c2e-6a1b-4c8f-9e2d-7b5a1c3d4e6f"] # optional: claim scope
84
+ rate_limit: 50 # default: requests per second
85
+ backfill: 7d # default: history at first assignment
86
+ overlap: 1h # default: window re-read for late samples
87
+ timeout: 30s # default: per request
88
+ ```
89
+
90
+ | Key | Requirement | Content |
91
+ |-----|-------------|---------|
92
+ | `token` | required | The personal access token the instance authenticates with, given as `${IBOS_PAT}` with the value in Bricklogger's [`env` file](https://cx1-aps.github.io/bricklogger/features/configuration/#the-env-file) like every secret |
93
+ | `url` | default `https://api.data.ibostechnologies.com` | The API's base URL |
94
+ | `projects` | optional | The projects the instance claims: a list of project numbers and UUIDs, matched against each reference's project node |
95
+ | `rate_limit` | default `50` | The instance's request budget in requests per second. The API allows 100 per token, and a token may be shared with other clients |
96
+ | `backfill` | default `7d` | How far back the first fetch of a point reaches; `0` fetches only what the cloud receives after the assignment |
97
+ | `overlap` | default `1h` | How far behind the latest known sample every round starts again, to catch samples the cloud received late |
98
+ | `timeout` | default `30s` | How long one request waits for its response |
99
+
100
+ A reference is in scope when its project's number or UUID is in `projects`.
101
+ Without `projects` the instance claims every iBOS reference — the simple
102
+ installation configures nothing but the token. The instance declares no
103
+ exclusive [resource](https://cx1-aps.github.io/bricklogger/architecture/#isolation-and-resources): two
104
+ instances with the same token and different projects are a legitimate way to
105
+ split a budget.
106
+
107
+ ## The reference
108
+
109
+ A point is addressed through Brick's external reference, as for every source:
110
+ the point carries `ref:hasExternalReference` to a reference node. Brick's
111
+ reference schema has no type for an object in a cloud service, so the
112
+ reference is written in the **iBOS vocabulary** described
113
+ [below](#the-ibos-vocabulary), namespace `https://brick.cx2.dk/schema/ibos#`
114
+ with the prefix `ibos:`:
115
+
116
+ ```turtle
117
+ @prefix ibos: <https://brick.cx2.dk/schema/ibos#> .
118
+
119
+ ex:Building_A a rec:Building, ibos:Project ;
120
+ ibos:project-id 4711 ;
121
+ ibos:project-uuid "0d3f5c2e-6a1b-4c8f-9e2d-7b5a1c3d4e6f" . # optional
122
+
123
+ ex:AHU_01_SAT a brick:Supply_Air_Temperature_Sensor ;
124
+ ref:hasExternalReference [
125
+ a ibos:Reference ;
126
+ ibos:object-uuid "3fa85f64-5717-4562-b3fc-2c963f66afa6" ;
127
+ ibos:project ex:Building_A ;
128
+ ibos:object-type "analog-input" ; # informational, checked
129
+ ibos:object-instance 3 ; # informational, checked
130
+ ibos:object-name "AHU01_SAT" ; # informational
131
+ ibos:device-uuid "9b2c7d1e-4f3a-4b5c-8d6e-1a2b3c4d5e6f" # informational
132
+ ] .
133
+ ```
134
+
135
+ - **The object:** `ibos:object-uuid` is the object's UUID in iBOS, the key
136
+ under which the API serves everything about it. The source recognises an
137
+ iBOS reference by this property, whether or not the node is typed
138
+ `ibos:Reference`: the vocabulary's rule derives the type at activation, as
139
+ Brick's own rules do for Brick's reference types.
140
+ - **The project:** `ibos:project` links the reference to the iBOS project the
141
+ object belongs to, a node with `ibos:project-id`, the project's number in
142
+ iBOS. The node is typically the building itself, typed `rec:Building` and
143
+ `ibos:Project` at once; `ibos:project-uuid` and a label are optional. The
144
+ project is the unit by which an instance decides which points it claims.
145
+ - **Informational properties:** `ibos:object-type`, in the standard's
146
+ hyphenated spelling, `ibos:object-instance`, `ibos:object-name` and
147
+ `ibos:device-uuid` describe the object as iBOS knows it. They play no part
148
+ in addressing, but the source reads the object's metadata from the API in
149
+ any case, and a type or instance number in the graph that contradicts the
150
+ API's makes the point `rejected`: the reference then almost certainly
151
+ carries the wrong UUID.
152
+
153
+ **What is rejected.** As for every source, a reference the source recognises
154
+ but cannot use gives the point the outcome `rejected` with a reason that
155
+ names the problem, and the point is not collected until the model is fixed.
156
+ That covers a node typed `ibos:Reference` without `ibos:object-uuid`, a
157
+ malformed UUID or instance number, a reference without `ibos:project`, a
158
+ project node without `ibos:project-id`, a type or instance number that
159
+ contradicts the API, and a point with two iBOS references of which neither is
160
+ marked `ref:preferred true`
161
+ — where one is marked, it is used. The rule is the same as for BACnet: a
162
+ point logged from the wrong object under the right name is worse than a
163
+ visible gap.
164
+
165
+ > [!NOTE]
166
+ > **Not a time-series reference.** Brick's `ref:TimeseriesReference` says
167
+ > where a point's time series is **stored**. It is reserved for the databases
168
+ > Bricklogger writes to and is never a source's address, so an iBOS object is
169
+ > not written as one.
170
+
171
+ ## The iBOS vocabulary
172
+
173
+ The vocabulary is a small extension of Brick's reference schema: one Turtle
174
+ document with the classes, properties and rules below, shipped in this
175
+ package. The namespace is an identifier; nothing needs to be fetched from it.
176
+ The iBOS source [declares it](https://cx1-aps.github.io/bricklogger/architecture/#declaration), so
177
+ the daemon loads it at every activation together with Brick's ontology and
178
+ rules — a model that uses it needs no network access — and the prefix `ibos:`
179
+ is pre-declared like `ref:` and `bacnet:`, in SPARQL and in prefixed names.
180
+
181
+ | Term | Kind | Content |
182
+ |------|------|---------|
183
+ | `ibos:Reference` | class, subclass of `ref:ExternalReference` | A reference to an object in iBOS |
184
+ | `ibos:Project` | class | An iBOS project; the node may also be the building or site itself |
185
+ | `ibos:object-uuid` | string | The object's UUID in iBOS. **Required** |
186
+ | `ibos:project` | link | The project the object belongs to. **Required** |
187
+ | `ibos:project-id` | integer | The project's number in iBOS, on the project node. **Required** |
188
+ | `ibos:project-uuid` | string | The project's UUID, on the project node. Optional |
189
+ | `ibos:object-type` | string | The BACnet object type in the standard's hyphenated spelling, e.g. `analog-input`. Informational, checked against the API |
190
+ | `ibos:object-instance` | integer | The BACnet instance number. Informational, checked against the API |
191
+ | `ibos:object-name` | string | The object's name in iBOS. Informational |
192
+ | `ibos:device-uuid` | string | The UUID of the device the object belongs to. Informational |
193
+
194
+ Two SHACL rules run at activation: a node under `ref:hasExternalReference`
195
+ that carries `ibos:object-uuid` is typed `ibos:Reference`, and the object of
196
+ `ibos:project` is typed `ibos:Project`.
197
+
198
+ ## Fetching
199
+
200
+ The source owns the rhythm, within a budget:
201
+
202
+ - **A round per point.** At the rule's `interval` the source asks the cloud,
203
+ per object, for the samples newer than the latest it has, minus the
204
+ `overlap`. Samples already delivered are dropped in the source, so a
205
+ repeated sample never leaves it; a sample the cloud received late, with a
206
+ timestamp inside the window, is delivered when it appears. The `interval`
207
+ thus bounds how far the copy lags behind the cloud, not the spacing of the
208
+ samples — that is the cloud's, and every sample keeps its own timestamp.
209
+ - **Where a point starts.** A point the daemon has never recorded an
210
+ observation for — new in the model, or a fresh runtime state — is fetched
211
+ `backfill` back from that moment. A point it has an observation for — after
212
+ a restart of the daemon or of the instance — is fetched from that latest
213
+ observation minus `overlap`, however long ago, so a restart refetches
214
+ nothing but the overlap and an outage is caught up in pages; the
215
+ [assignment](https://cx1-aps.github.io/bricklogger/architecture/#operation) carries the timestamp. From then
216
+ on only what is new. A point with no new samples in a
217
+ round yields nothing: the age of its last value in status shows that the
218
+ cloud is silent.
219
+ - **Pages.** The API serves at most 10 000 samples per request, the newest
220
+ first; a longer stretch — the first fetch, or the catch-up after an outage
221
+ — is read in pages, each reaching further back than the one before, and
222
+ each counting against the budget.
223
+ - **The budget.** Requests are spread evenly under `rate_limit`, and nothing
224
+ else is configurable. A round takes at least as long as its requests need —
225
+ 3 000 objects at 50 requests per second take a minute — and **rounds never
226
+ pile up**: the next round starts when the previous one has finished, and a
227
+ round that outlasts the interval gives the warning `rate_limited` per
228
+ instance, with the cadence the instance can actually keep. Nothing is lost
229
+ meanwhile, because the cloud keeps the history: the budget costs latency,
230
+ never samples. A `429` from the API is honoured — the source waits the
231
+ `Retry-After` it names — and counted in status.
232
+
233
+ Capacity follows from the budget: the points an instance serves, each divided
234
+ by its interval in seconds, must add up to less than `rate_limit` for the
235
+ rounds to keep their intervals — 50 requests per second serve 15 000 points
236
+ every five minutes, or 3 000 every minute.
237
+
238
+ **Timestamps are the cloud's.** Every observation carries the sample's
239
+ timestamp as the API gives it, in UTC; the source never stamps a sample
240
+ itself. An older sample can therefore arrive after a newer one, and the
241
+ daemon's *last known value* is the latest by timestamp, not by arrival, as the
242
+ [architecture](https://cx1-aps.github.io/bricklogger/architecture/#the-working-graph) describes.
243
+
244
+ ## Value types and units
245
+
246
+ An object's type — `object_type` in the API, the BACnet object type — decides
247
+ the value type, with the same table as [BACnet/IP](https://cx1-aps.github.io/bricklogger/features/sources/#value-types-and-units).
248
+ A sample carries `value`, a number, and `value_text`, a text, and the type
249
+ says which of them is the value:
250
+
251
+ | Object type | Value type | Value and texts |
252
+ |-------------|------------|-----------------|
253
+ | analog-input, analog-output, analog-value, large-analog-value, loop | `number` | `value` |
254
+ | integer-value, positive-integer-value, accumulator | `integer` | `value`, as an integer |
255
+ | binary-input, binary-output, binary-value | `boolean` | `value` 1 is true, 0 false; a text in `value_text` that is not the number itself becomes the state's text |
256
+ | multi-state-input, multi-state-output, multi-state-value | `enum` | `value` is the state, counting from 1; a text in `value_text` that is not the number itself is learned as the state's text and delivered as metadata as it appears |
257
+ | character-string-value | `string` | `value_text` |
258
+ | date-time-value | `datetime` | `value_text`, as read; stored in UTC |
259
+ | Everything else | — | The point is `rejected`: the datatype has no counterpart in the vocabulary |
260
+
261
+ A sample whose `value` and `value_text` are both empty is delivered as `null`
262
+ with reason `no_value`: the cloud recorded that the collector had no value at
263
+ that time, and the time series says so. In the API as observed, `value_text`
264
+ repeats the number — `"3"` beside `3.0` — so states stay ordinals.
265
+
266
+ **Units.** The object's unit comes with its metadata from the API as
267
+ `unit_id`, BACnet's engineering-unit number, and as the text `units`, a
268
+ symbol such as `°C`, `%` or `m³/h`. The source translates the number into
269
+ Brick's unit vocabulary (QUDT) with the same table as BACnet/IP, translates
270
+ the symbol where it knows it — the objects list carries the symbol alone —
271
+ and otherwise delivers the text as the protocol's own designation. As for
272
+ every source, the graph's unit stays primary, a
273
+ disagreement gives the `unit_conflict` warning, and values are never
274
+ converted.
275
+
276
+ **When metadata is read.** The object's metadata — type, instance, name and
277
+ unit — is read from the API once when a point is assigned. It settles the
278
+ value type and the unit, and it is where a graph's `ibos:object-type` or
279
+ `ibos:object-instance` is checked; a contradiction gives `rejected`. The
280
+ enumeration's texts follow as the states appear in the data.
281
+
282
+ ## Failed requests
283
+
284
+ The source delivers only what the cloud has recorded. When a request fails,
285
+ it delivers **nothing** for that point in that round and fetches the whole
286
+ stretch when the next request succeeds, so the time series stays complete and
287
+ carries no marks of the logger's own outages. What went wrong shows in
288
+ status, per project:
289
+
290
+ | Answer | Effect |
291
+ |--------|--------|
292
+ | `404` for the object | The point is `rejected` with the reason that the object is not in iBOS: the reference carries the wrong UUID, or the object is gone |
293
+ | `403` for the project | The project is unreachable in status with the error; its points stay `active` and receive nothing until the token has access again. A token's permissions are not a model error |
294
+ | `401` | The token is invalid: the instance is `failed` with the error, and the daemon restarts it with backoff |
295
+ | `429` | The source waits the `Retry-After` the API names and counts the answer; see [fetching](#fetching) |
296
+ | `5xx`, timeout, no connection | The project is unreachable in status until a request succeeds |
297
+
298
+ This is where a history source differs from a live one: BACnet/IP delivers
299
+ `null` with `unreachable` for every failed poll, because nothing else records
300
+ that the value was not available, whereas the cloud's history records it
301
+ itself.
302
+
303
+ ## Protocol tools
304
+
305
+ The tools run with `bricklogger sources <instance> <tool>` — through the daemon
306
+ when it runs, in-process otherwise — as described for
307
+ [protocol tools](https://cx1-aps.github.io/bricklogger/architecture/#protocol-tools) in general. Every tool
308
+ returns a structured result that the CLI renders as a table or emits as JSON.
309
+
310
+ | Tool | Parameters | Result |
311
+ |------|------------|--------|
312
+ | `projects` | — | The projects the token has access to: number, UUID, name, description and the counts of devices and objects |
313
+ | `devices` | `--project`, number or UUID | The project's devices: UUID, device number, name, vendor, model and IP address |
314
+ | `objects` | `--device` UUID; `--values` adds the latest sample | The device's objects: UUID, type, instance, name, unit and its QUDT counterpart, optionally the latest sample |
315
+ | `read` | `--object` UUID; `--since` (default `1h`) and `--limit` (default `100`) bound the samples | The object's latest samples as the vocabulary sees them — timestamp, type and value — with the raw `value` and `value_text` beside |
316
+ | `resolve` | `--point`, a URI in full or prefixed form | How the point's reference resolves — object UUID, project and whether it is in the instance's scope — the object's metadata from the API against the graph's type and instance, and the latest sample, or the problem with the reference |
317
+ | `pointlist` | `--project`, number or UUID, optional | The point list: the projects the instance claims, or the one project, with their devices and the objects on each, as one JSON document to keep as a file |
318
+
319
+ `projects`, `devices`, `objects`, `read` and `pointlist` need nothing but the
320
+ instance's configuration, so they also run without a daemon. `resolve` needs
321
+ the running daemon, because the working graph is the daemon's. No tool writes
322
+ to the cloud; the API is read-only.
323
+
324
+ ### The point list
325
+
326
+ The point list is what the API has, without a single sample: the projects,
327
+ their devices and the objects on each device — an inventory of what the token
328
+ can see, to file with the building's documentation or to hand to whoever
329
+ builds the Brick model. It is a [document](https://cx1-aps.github.io/bricklogger/architecture/#protocol-tools),
330
+ so the CLI writes it as JSON, to standard output or to a file with `-o`:
331
+
332
+ ```
333
+ bricklogger sources ibos_main pointlist --project 4711 -o building-a.json
334
+ ```
335
+
336
+ In the web interface the point list is offered on the `projects` listing: run
337
+ `projects`, and every project row carries a Download for that project's point
338
+ list, with a Download for the whole list — every project the instance claims —
339
+ above the table. The tool has no form of its own there.
340
+
341
+ Without `--project` the list covers the projects the instance claims — the
342
+ `projects` setting, or every project the token has access to when the setting
343
+ is absent. With `--project` it covers that one project, whether or not the
344
+ instance claims it. A project the token cannot see fails the tool, as it fails
345
+ `devices`.
346
+
347
+ ```json
348
+ {
349
+ "instance": "ibos_main",
350
+ "url": "https://api.data.ibostechnologies.com",
351
+ "exported_at": "2026-09-09T13:05:12+00:00",
352
+ "counts": {"projects": 1, "devices": 1, "objects": 2},
353
+ "projects": [
354
+ {
355
+ "number": 4711,
356
+ "uuid": "0d3f5c2e-6a1b-4c8f-9e2d-7b5a1c3d4e6f",
357
+ "name": "Building A",
358
+ "description": null,
359
+ "devices": [
360
+ {
361
+ "uuid": "9b2c7d1e-4f3a-4b5c-8d6e-1a2b3c4d5e6f",
362
+ "number": 1201,
363
+ "name": "Supervisor",
364
+ "vendor": "Tridium",
365
+ "model": "JACE-8000",
366
+ "ip": "192.168.10.20",
367
+ "objects": [
368
+ {"uuid": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "type": "analog-input", "instance": 3, "name": "AHU01_SAT", "unit": "°C", "qudt": "http://qudt.org/vocab/unit/DEG_C"},
369
+ {"uuid": "7c9e6679-7425-40de-944b-e07fc1f90ae7", "type": "binary-input", "instance": 1, "name": "AHU01_FAN", "unit": null, "qudt": null}
370
+ ]
371
+ }
372
+ ]
373
+ }
374
+ ]
375
+ }
376
+ ```
377
+
378
+ The header names the instance, the API's URL, the time of the export in UTC
379
+ and the counts; then follow the projects, each with its devices, each with its
380
+ objects. The fields are those of `projects`, `devices` and `objects` — a
381
+ project without its two counts, since the lists themselves are there — with
382
+ the object type in the standard's hyphenated spelling, so the document reads
383
+ as the three lists nested. Building it takes one request per project and one
384
+ per device, under the instance's budget as always; a project with thousands of
385
+ objects is a handful of requests.
386
+
387
+ ## Reporting a problem
388
+
389
+ Bugs and questions go in the
390
+ [issues](https://github.com/CX1-ApS/bricklogger-ibos/issues). A security
391
+ vulnerability is reported privately, as
392
+ [SECURITY.md](https://github.com/CX1-ApS/bricklogger-ibos/blob/main/SECURITY.md)
393
+ describes.
394
+
395
+ ## License
396
+
397
+ [MIT](https://github.com/CX1-ApS/bricklogger-ibos/blob/main/LICENSE). iBOS is a
398
+ product of iBOS Technologies; this plugin is not affiliated with it.