@thinger-io/cli-core 0.3.0 → 0.4.0

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,72 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.0] - 2026-10-16
4
+
5
+ ### Added
6
+
7
+ - `dashboard init <dir>` and MCP tool `dashboard_init`: a new dashboard app
8
+ from a template that `@thinger-io/dashboard-sdk` ships (`--template`,
9
+ React by default), downloaded from npm (latest release, integrity
10
+ checked, extracted without a `tar` binary) or, offline, taken from the
11
+ copy cli-core depends on; `--from` takes a version or tag, or a local
12
+ folder or .tgz of the package. The app gets the SDK as a dependency, the
13
+ manifest (storage `app_<folder>_<random>`), an AGENTS.md (and CLAUDE.md
14
+ pointing at it) with the rules of the console's frame and how to review
15
+ the layout, and with `--ci` a GitHub workflow that publishes it
16
+ (`npmPackage` in the identity names the CLI it runs).
17
+ - `dashboard dev [url]` and MCP tool `dashboard_dev`: develop a dashboard
18
+ app on its own dev server (ng serve, Vite…) against a real server. It
19
+ returns a link to the local app that opens it as a standalone page with
20
+ the server (`thinger_api`), a temporary full-access token
21
+ (`dashboard_dev[_<project>]`, 8 h by default, replaced by the next link)
22
+ and the context of a destination in its fragment. The url must be on
23
+ this machine unless `--allow-remote` (`allow_remote`); it says when
24
+ nothing answers there.
25
+ - `dashboard publish [dir]`: the command for `publishApp`, with
26
+ destinations as `device:<id>`, `dashboard:<id>`…
27
+ - An app's manifest (`<cli>.json`, section `dashboard`: `dir`, `storage`,
28
+ `prune`, `to`, `dev.url`, `dev.context`, `dev.route`, `dev.hours`) gives
29
+ these commands their defaults; `dir` must stay inside the manifest's
30
+ folder and the fields are type-checked.
31
+ - SDK compatibility: publish and dev read the SDK the app is built with
32
+ (`node_modules/@thinger-io/dashboard-sdk`, from the build or project
33
+ folder up) and its `compat.json`, and warn when the console gives it no
34
+ token (SDK before 0.3.0), when the profile's server
35
+ (`/v1/server/version`) is older than the SDK or some of its calls need,
36
+ when npm has a newer SDK, or when the page loads the deprecated script
37
+ build.
38
+ - `<PREFIX>_TOKEN` and `<PREFIX>_SERVER` stand for a profile (CI) unless a
39
+ profile is named; the token never goes to the identity's default server,
40
+ and `logout` says so instead of logging out of nothing.
41
+
42
+ ### Changed
43
+
44
+ - Documentation no longer comes from the server (which stops serving
45
+ `/sdk/`), with the same ids: the platform's guides (`concepts`,
46
+ `product-code`, `server-admin`) ship in cli-core (`docs/`), marking what
47
+ needs a given server version ("since Thinger 7.5"); the dashboard SDK's
48
+ (`dashboard-sdk`, the reference, and `dashboard-sdk-types`) come from its
49
+ package: the version installed in the project of the working folder,
50
+ else the CLI's own (cli-core depends on `@thinger-io/dashboard-sdk`).
51
+ `doc_read`, the MCP resources and `resource_describe` read them without
52
+ a request.
53
+ - Preview and dev links refuse a route with `&` or `#`, which could add
54
+ parameters of its own to the fragment.
55
+
56
+ ### Removed
57
+
58
+ - `FALLBACK_DOCS` and `validDoc` (docs.js): no document is read from the
59
+ server.
60
+ - The layout check link of preview links (`layoutUrl`, `LAYOUT_SIZES`):
61
+ the server retires `/sdk/preview.html`. The AGENTS.md of `dashboard init`
62
+ says how to review the layout instead.
63
+
64
+ ### Fixed
65
+
66
+ - The catalog note on dashboards named `target` where `dashboard_publish`
67
+ takes `to`.
68
+ - `defineCliDocs` files given as `URL` resolve on Windows too.
69
+
3
70
  ## [0.3.0] - 2026-10-14
4
71
 
5
72
  ### Added
package/README.md CHANGED
@@ -12,16 +12,29 @@ It holds everything that is not specific to a product:
12
12
  pasted token, OAuth device flow; interactive or fully non-interactive.
13
13
  - **Profiles** (`config`) — several servers per CLI, selected by `--profile`,
14
14
  `<PREFIX>_PROFILE` or the saved default, and scoped per call
15
- (`withActiveProfile`) so concurrent MCP calls don't mix servers.
15
+ (`withActiveProfile`) so concurrent MCP calls don't mix servers. In CI,
16
+ `<PREFIX>_SERVER` and `<PREFIX>_TOKEN` stand for a profile without any
17
+ file.
16
18
  - **REST helpers** — `devices`, `property`, `resource`, `bucket`, `product`,
17
19
  `profile` (product profiles), `tokens`, `alarms`, `storage`, with every
18
20
  account path built in `paths`.
19
21
  - **Generic commands** (`cli`, `commands/*`) — login, logout, profile, device,
20
- product, bucket, mcp.
21
- - **MCP** (`mcp/*`) — the stdio server and 23 generic tools: eight
22
+ product, bucket, dashboard, mcp.
23
+ - **Dashboard apps** (`dashboard`, `dashboard-init`, `dashboard-dev`,
24
+ `dashboard-sdk`) — create an app from a template of
25
+ `@thinger-io/dashboard-sdk`, develop it on its own dev server (a link with
26
+ a temporary token opens the local app against the server), and publish
27
+ its build to a public storage that dashboards point at, with a warning
28
+ when its SDK needs a newer server or is not the latest.
29
+ - **Documentation** (`docs`, `docs/`) — the guides for agents: the
30
+ platform's concepts, product code and server administration (shipped
31
+ here, marking what needs a given server version), the dashboard SDK's
32
+ reference and types (from its package: the project's version, else the
33
+ CLI's), and the CLI's own.
34
+ - **MCP** (`mcp/*`) — the stdio server and 25 generic tools: eight
22
35
  `resource_*` tools over a catalog of the server's resource types, and the
23
36
  operations that are not CRUD (bucket data, device resources, storage
24
- files, dashboard publishing, the server's documentation…), plus that
37
+ files, dashboard development and publishing, the documentation…), plus that
25
38
  documentation as MCP resources.
26
39
  - **OpenAPI** (`openapi`) — the server's own API description, cached per
27
40
  server and version, with a bundled copy as fallback.
@@ -40,7 +53,7 @@ defineIdentity({
40
53
  displayName: 'Acme IoT',
41
54
  description: 'CLI for Acme IoT servers',
42
55
  configDir: 'acme-cli', // ~/.config/acme-cli/config.json
43
- envPrefix: 'ACME', // ACME_PROFILE, ACME_INSECURE
56
+ envPrefix: 'ACME', // ACME_PROFILE, ACME_INSECURE, ACME_SERVER + ACME_TOKEN
44
57
  token: { id: 'acme_cli', name: 'Acme CLI' }, // minted by the password login
45
58
  oauth: { clientId: null }, // OAuth device-flow client, if the platform ships one
46
59
  defaultServer: null, // suggested by the login prompt
@@ -59,7 +72,7 @@ servers.
59
72
 
60
73
  `runCli` takes:
61
74
 
62
- - `register(program, { device, product, bucket })` — add top-level commands,
75
+ - `register(program, { device, product, bucket, dashboard })` — add top-level commands,
63
76
  or subcommands inside the generic groups.
64
77
  - `onNoCommand(program)` — what to do when called without arguments
65
78
  (default: help).
@@ -0,0 +1,389 @@
1
+ # Thinger concepts
2
+
3
+ How a Thinger.io account is organized, for agents that create resources, dashboards or
4
+ alarms. It describes Thinger 7.5: what needs a later version says "since Thinger X", and what
5
+ older servers lack says "since Thinger 7.5" (a server's version: `GET /v1/server/version`).
6
+ `GET /v1/server/events` lists the live events; the OpenAPI document lists every route. Related
7
+ guides: product code (document `product-code`) and server administration (document
8
+ `server-admin`). The dashboard SDK is the npm package `@thinger-io/dashboard-sdk`, documented in
9
+ its README (document `dashboard-sdk`).
10
+
11
+ ## The account and its projects
12
+
13
+ Everything (devices, buckets, products, dashboards…) belongs to a **user account** and lives
14
+ under `/v{1,2,3}/users/<user>/…`.
15
+
16
+ A **project** groups resources of the account so they can be shared and browsed together:
17
+
18
+ - A resource belongs to projects through its `project` field, an array of `"<owner>@<project>"`.
19
+ One resource can be in several projects. Devices, products, buckets, dashboards, endpoints,
20
+ storages, access tokens, asset types and groups, alarm rules and instances, plugins, claims
21
+ and syncs can be in projects; properties follow their resource.
22
+ - `?project=<id>` on an API call scopes it: creates add the resource to the project, lists only
23
+ return its resources. `PUT <resource path>/projects` with `["<owner>@<project>", …]` sets the
24
+ list, **replacing** the previous one: read it first to add or remove one project.
25
+ - **Members** are other users invited to a project, with permissions from their own allow/deny
26
+ plus project and global roles. They call `/v1/users/<owner>/…?project=<id>`; the server
27
+ checks that the target resource is in the project and runs the call as the owner. A member
28
+ can also be limited to some asset groups.
29
+ - A project has its own properties. Its `dashboard` property
30
+ (`/v1/users/<owner>/projects/<project>/properties/dashboard`) is the project's home page:
31
+ members land on it when they log in, and the console menu shows it under the project name.
32
+ A member sees only the resources in the project, and only those its permissions allow.
33
+ - **Bucket data inside a project** (`config.limit_bucket_access`): a bucket in a project may also
34
+ hold records of devices outside it, typically a product bucket shared by several projects.
35
+ With this option on, every bucket read and tag listing made over the project
36
+ (`?project=`, members, project-scoped tokens and the dashboard SDK) only returns the records
37
+ of the project's devices, and asking for another device is refused. It needs the bucket to
38
+ carry a `device` tag (product buckets do); a bucket without it is not filtered. A project with
39
+ no devices reads such a bucket as empty. It is **on by default for new projects** since Thinger
40
+ 7.5 (send
41
+ `config: {limit_bucket_access: false}` to opt out); for older projects, turn it on with
42
+ `PUT /v1/users/<u>/projects/<p>` and `{config: {limit_bucket_access: true}}`, especially before
43
+ sharing a project with members or public tokens.
44
+
45
+ ## Devices
46
+
47
+ A device is one connected thing. `type` decides how it talks to the platform:
48
+
49
+ | type | connection | notes |
50
+ |---|---|---|
51
+ | `Generic` | IOTMP (Thinger client libraries) | has credentials; exposes its own resources |
52
+ | `MQTT` | MQTT broker | has credentials; publishes topics |
53
+ | `HTTP` | posts JSON to its callback URL | no persistent connection |
54
+ | `Virtual` | server-side, connected while enabled | no hardware; every resource comes from its product |
55
+ | `Subdevice` | through a parent device (`parent`) | e.g. sensors behind a gateway |
56
+
57
+ Other fields: `name`, `description`, `product`, `asset_type`, `asset_group`, `labels`
58
+ (free key → string map), `enabled`, and `connection` (`active`, `ts`, `location`…).
59
+
60
+ A device has three kinds of data. Do not mix them up:
61
+
62
+ - **Resources**: the device's live API (`/v3/…/devices/<d>/resources/<r>`). Reading one asks
63
+ the device right now; calling one runs an action on it. They can be streamed every N
64
+ seconds or on change. They only work while the device is connected. When it is offline,
65
+ `?shadow` returns the last value saved at disconnection. For HTTP, MQTT and Virtual devices,
66
+ resources only exist through their product's API.
67
+ - **Properties**: stored JSON values about the device (configuration, state, metadata, a
68
+ `location`). They persist while the device is offline and can be changed from the cloud.
69
+ The `location` property (`{ "lat": 40.41, "lon": -3.70 }`, numbers; `lng`, `latitude` /
70
+ `longitude` and an optional `radius` are also accepted) fixes the device on maps
71
+ (`devices.locations`, `GET /v1/users/<u>/assets/locations`). Devices without it are placed by
72
+ their IP when they connect, which is meaningless for Virtual devices: set `location` on
73
+ them to place them on a map.
74
+ - **Buckets**: time series. A device does not own buckets; its data is written into buckets.
75
+
76
+ ## Products
77
+
78
+ A product is a template for a family of devices. A device joins it through its `product`
79
+ field, set directly or by **autoprovision**: a device id pattern that creates the device the
80
+ first time it connects. The product profile defines for all of its devices:
81
+
82
+ - `properties`: device properties with defaults, forms and permissions; can also be filled
83
+ from MQTT topics, resource streams or events. An entry with `default.value` (any value but
84
+ `null`: `{}`, `[]`, `""`, `0` and `false` count), `form` or `perm` becomes a property of the
85
+ product, which every device of the product inherits (`source: "product"`) until it sets its own.
86
+ - `buckets`: where each device's data is stored. A bucket id containing `{{device}}` gives one
87
+ bucket per device. Otherwise one shared bucket is used, tagged with `device` (and `group`).
88
+ These buckets are created on the first write, have `product=<p>` and are not in any project:
89
+ projects get resources only by explicit assignment. Pick the access by use case:
90
+ - one device (device and product dashboards): read through the device
91
+ (`GET /v2/users/<u>/devices/<d>/buckets/<b>/data`, permission `Device:ReadDeviceBucket`; the
92
+ SDK's `device` option). Access to the device is enough; the bucket stays out of the project.
93
+ Prefer this whenever it covers the need.
94
+ - the whole fleet (project dashboards, aggregates across devices): add the bucket to the project.
95
+ - `api`: resources added to every device of the product. They map to topics, device resources,
96
+ properties, functions or endpoint calls, so HTTP and MQTT devices get an API too.
97
+ - `flows`: move data from a source (topic, resource, event…) to a sink.
98
+ - `code`: JavaScript functions used by the profile: values computed by code, payload
99
+ transforms, `device_id_resolver`… See the document `product-code`.
100
+ - `autoprovisions`, and `services` (web / VNC proxies to the device).
101
+
102
+ `POST /v1/…/products/<p>/resources/<r>` calls a product API resource without naming a device.
103
+ It only reaches a device when that resource defines a `device_id_resolver` function.
104
+
105
+ **API resources** (`profile.api.<resource>`) are what a device of the product can be asked to do.
106
+ `request.data.target` says where the call goes:
107
+
108
+ | target | does | typical use |
109
+ |---|---|---|
110
+ | `topic` | publishes the payload on an MQTT `topic` (templated, e.g. `shellies/{{device}}/relay/0/command`) | commanding MQTT devices |
111
+ | `resource` | calls a resource of the device (firmware) or another API resource | wrapping or renaming device resources |
112
+ | `resource_stream` | emits the payload as data of this resource, without a device behind it | data pushed by a plugin or an HTTP callback (e.g. a LoRaWAN `uplink`), consumed by profile buckets / properties with `source: "resource"` |
113
+ | `property` | stores the payload in the device property `property` | settings sent from a dashboard; state |
114
+ | `function` | runs a function of the product code | values computed by code, simulated devices |
115
+ | `endpoint_call` | calls an endpoint (email, webhook…) | notifications, external services |
116
+ | `plugin_endpoint` | calls an HTTP path of a plugin (`plugin`, `path`) | network servers, e.g. a LoRaWAN `downlink` |
117
+ | `product_stream` | emits a product-level stream | fan-out to the whole product |
118
+
119
+ `response.data.source` says what the caller gets back: `request_response` (the target's
120
+ result), `property`, `resource`, `topic` (waits for a message on a topic), `function` or nothing.
121
+ Payloads are templates: `{{payload.port=85}}` reads input with a default, `{{property.x}}`
122
+ reads device state, `{{payload:fn}}` converts with a product code function. A product-level call
123
+ (`POST /v1/…/products/<p>/resources/<r>`) needs `device_id_resolver` to know the device.
124
+
125
+ The plugin catalog (`GET /v1/users/<u>/plugins?filter=available`) ships complete products for
126
+ real hardware (Shelly, Milesight, Siemens LOGO, LoRaWAN templates…): each manifest has
127
+ `resources.products[].profile` with working API resources, buckets and properties to learn from
128
+ or reuse.
129
+
130
+ **Devices without hardware.** A `Virtual` device of a product gets each of its resources from
131
+ the product API, and an API resource can be computed by a function of the product code. A
132
+ profile bucket (or property) with `source: "resource"`, `update: "interval"` calls that resource
133
+ on every connected device of the product, Virtual ones included, so data flows with nothing
134
+ outside the server. Combined with a history import (Buckets), this gives realistic fleets for
135
+ demos and tests.
136
+
137
+ ## Buckets
138
+
139
+ A bucket stores timestamped records. Where its data comes from (`config.source`):
140
+
141
+ - `device`: the server reads a device resource every `interval` seconds, or on its events.
142
+ - `mqtt`: messages published on a topic.
143
+ - `write` / `api`: anyone with permission writes it (API, device firmware, HTTP callbacks,
144
+ product handlers).
145
+
146
+ `config.tags` (up to 5) are fields indexed for filtering, e.g. `device` or `site`. They are
147
+ read with `tag_<name>=<value>`, and their values can be listed. A **device bucket**
148
+ (`/v3/…/devices/<d>/buckets/<b>`) is not separate storage: it is the bucket filtered by
149
+ `device=<d>`, and it is read-only. That filter needs a `device` tag: product buckets have it
150
+ automatically; a bucket you create yourself needs `config.tags: ["device"]`, or per-device reads
151
+ come back empty. Backends: MongoDB, InfluxDB, DynamoDB; `retention` sets how long data is kept.
152
+
153
+ **Product profile buckets** (`profile.buckets.<id>`): with `data.source: "resource"`,
154
+ `data.resource`, `update: "interval"`, `interval` + `magnitude` (`second`, `minute`, `hour`…) and
155
+ either `payload_type: "source_payload"` or `payload: "{{payload}}"` (the two are equivalent), the
156
+ server samples that resource on each device of the product and writes the result. With neither,
157
+ the payload is the default `template_payload` with an empty template, and nothing is written.
158
+ Records get a `device` field; the bucket is created on the first write.
159
+
160
+ **Field names**: `ts` is the record time. In MongoDB buckets `tags` and `_id` are reserved (they
161
+ hold the record's tags and id): a record with a field of that name is refused, so rename it
162
+ (`tag_count`, …). Since Thinger 7.5 the refusal names the field.
163
+
164
+ **Reading** (`GET /v2/users/<u>/buckets/<b>/data`, or the SDK's `buckets.read`): records come as
165
+ `{ ts, ...fields }`, newest first. Query: `min_ts` / `max_ts` (ms), `items` (default 50, **at most
166
+ 1000** per call), `sort` (`desc` / `asc`), `agg` (`5m`, `1h`, `1d`…) with `agg_type` (`mean`,
167
+ `min`, `max`, `sum`, `count`…), `group_by` (e.g. `device`), `fields`, `tag_<name>=<value>`
168
+ (repeat for several values), `tz`. For long ranges, aggregate; to read everything, page backwards with
169
+ `max_ts` = oldest `ts` received − 1. With `agg` or `group_by`, each of the `fields` is reduced on
170
+ its own (`group_by` without `agg_type` gives each field's latest value): a record or group that
171
+ lacks one field still counts for the others, and a field without values is left out of the row.
172
+ Without `fields`, an aggregation covers the **numeric** fields of the latest record in the range
173
+ (nested ones too, up to 64): name string fields explicitly in `fields` to get them with `last`,
174
+ `first`, `mode` or `count`.
175
+
176
+ Aggregation windows (`1h`, `1d`, `1w`…) start at midnight, hour boundaries, etc. of the time zone
177
+ in `tz` (IANA name, e.g. `Europe/Madrid`); without it, **UTC**. So daily totals for a site need
178
+ `tz` set to the site's zone, or the days split at UTC midnight. The dashboard SDK sends the
179
+ viewer's browser zone unless told otherwise (`timeZone` option). A record falls in the window that contains its
180
+ `ts`, so a window covers [start, start + period): when importing pre-aggregated history (e.g. one
181
+ record per 5 minutes), stamp each record with the START of the period it summarizes.
182
+
183
+ **Writing history**:
184
+
185
+ - `POST /v1/users/<u>/buckets/<b>/data` with an object or an array of objects; a numeric `ts`
186
+ (ms) in each object sets its time (MongoDB and InfluxDB).
187
+ - `POST /v1/users/<u>/buckets/<b>/imports` with
188
+ `{ "source": "storage", "storage": "<id>", "file": "<path>.csv" }` loads a CSV from a file
189
+ storage into an existing bucket. First row is the header; separator `,` or `;`. A `ts` (or
190
+ `date` / `timestamp`) column holds the time in **milliseconds** or ISO 8601 (UTC); seconds are
191
+ misread as milliseconds. Other columns become fields (numbers when they parse), and a column
192
+ named like a bucket tag (e.g. `device`) becomes that tag. The response streams
193
+ `{"progress": N}` lines. Not for DynamoDB buckets.
194
+
195
+ ## Asset types and asset groups
196
+
197
+ Classification shared by devices (and buckets):
198
+
199
+ - **Asset type** (`asset_type`): what a device is (boiler, meter…).
200
+ - **Asset group** (`asset_group`): where or whose it is. Groups nest (`parent`), and project
201
+ members can be limited to some groups.
202
+
203
+ Both are created on the fly when a device references them. Both have properties that their
204
+ devices inherit, and both have location endpoints for maps.
205
+
206
+ ## Properties and inheritance
207
+
208
+ Devices, products, projects, asset types and asset groups all have properties. When reading
209
+ a device property, the first level that defines it wins: **device → asset type → asset group →
210
+ product** (the device's own group, not its parent groups). The whole value is taken from that
211
+ level: values are not merged, so a property set on the device hides the one of its group or
212
+ product. The same lookup applies to `{{property.<id>}}` in product templates, so a setting shared
213
+ by many devices (a schedule, a threshold) can live on their group or product and be overridden on
214
+ one device. The product can force permissions (`perm`: hidden, read-only for members). The property
215
+ `dashboard` on any of them is the dashboard the console shows for that resource. A `null` inside a
216
+ stored value currently reads back as `""` (empty string): use another marker, or leave the key out,
217
+ when null matters.
218
+
219
+ ## Geofences
220
+
221
+ Since Thinger 7.5. The server checks geofences natively: do not compute zone entries and exits in your own code. A
222
+ property `geofences` on the device, or inherited from its type, group or product, holds a GeoJSON
223
+ `FeatureCollection`. Each `Feature` is a `Polygon`, or a `Point` with `properties.radius` in
224
+ meters. **Coordinates are `[lon, lat]`**, as in GeoJSON (the `location` property is `{lat, lon}`).
225
+ The whole collection comes from the first level that defines it: a device's own `geofences` replaces
226
+ its product's, they are not merged. Give each feature a stable GeoJSON `id` (or `properties.id`), so
227
+ renaming a zone does not break its history. Its `properties`:
228
+
229
+ - `name`: the display name, and the key of the zone in events (`geofence`);
230
+ - any other field of your own (customer, kind, schedule…): events carry them;
231
+ - `enabled` (default true);
232
+ - `onEnter`, `onExit`, `whileInside`, `whileOutside`: one entry per event to emit. The value is an
233
+ endpoint to call with the event, or `""` to emit the event only. Zones without them are
234
+ silent. The `while*` events fire on every location update: avoid them on buckets and endpoints
235
+ unless updates are rare.
236
+
237
+ Every change of the device's `location` (its property, also when a product property or a flow
238
+ writes it, or the location an HTTP device or subdevice sends) is checked against the previous one,
239
+ which is stored, so a server restart does not repeat events. Each transition emits
240
+ `device_geofence_event`, with `device`, `product`, `asset_type`, `asset_group`, `geofence` (name),
241
+ `geofence_id`, `geofence_properties` (the feature's own properties), `geofence_event` (onEnter…),
242
+ `latitude` and `longitude`. When one update leaves a zone and enters another, the exits are
243
+ emitted first. Changing or removing zones emits nothing: a device inside a zone that is removed gets
244
+ no `onExit`.
245
+
246
+ To keep a history, add a product bucket fed by that event:
247
+ `"data": {"source": "event", "event": "device_geofence_event", "payload_type": "source_event"}`.
248
+ To react to it, add a product flow with the same source and, for example, an `endpoint_call` sink, or
249
+ a `property` sink that keeps the last transition on the device. Event sources need
250
+ `"payload_type": "source_event"` in flows too, or nothing reaches the sink:
251
+
252
+ ```json
253
+ {
254
+ "name": "Last geofence transition",
255
+ "enabled": true,
256
+ "data": { "source": "event", "event": "device_geofence_event", "payload_type": "source_event" },
257
+ "sink": {
258
+ "target": "property",
259
+ "property": "last_zone",
260
+ "payload_type": "template_payload",
261
+ "payload": "{\"zone\": \"{{payload.geofence_id}}\", \"event\": \"{{payload.geofence_event}}\"}"
262
+ }
263
+ }
264
+ ```
265
+
266
+ In a sink template, `{{payload.<field>}}` reads the event and `{{property.<id>.<field>}}` a property
267
+ of the device.
268
+
269
+ ## Alarms
270
+
271
+ - An **alarm rule** checks sources every `check_interval` (must be > 0). Each source has a
272
+ `name` and is one of:
273
+ - `bucket`: `{ id, mapping: [fields], group_by: ["device"], aggregation: { period, type } }`.
274
+ `group_by: ["device"]` gives one value per device (needed to raise per-device alarms). The
275
+ period is `<n>m`, `h`, `d`, `w`, `mo` or `y` (defaults to the check interval); types `mean`,
276
+ `min`, `max`, `sum`, `count`, `median`, `spread` (max − min, a simple rate of change)…
277
+ Each field of `mapping` is reduced on its own: a device whose records lack one of them still
278
+ gets the others, and the missing one is absent.
279
+ - `device_property`: `{ property, fields: [...] }`, resolved with inheritance (device → asset
280
+ type → asset group → product). The value must be an object, and **fields are paths in the
281
+ property document**, so a value field is `value.<field>`: `fields: ["value.level"]` is read as
282
+ `battery.value.level`.
283
+ - `device_status`: the device's `enabled`, `connection.active`, `connection.ts` (time of the
284
+ last connect / disconnect, not of the last data).
285
+ - Every source can be filtered by `device`, `product`, `asset_type`, `asset_group`, `project`.
286
+ Only **enabled** devices are evaluated: disabling a device takes it out of its alarms.
287
+ - Sources are **joined per device**: each one yields values per device id, and a device's
288
+ conditions see its own values from every source. A bucket source does so with
289
+ `group_by: ["device"]` over the bucket's `device` **tag**, whose value is the device id.
290
+ Product buckets have it by default; give your own buckets `config.tags: ["device"]` too. It is
291
+ what makes per-device reads and filters efficient, and InfluxDB buckets can only group by tags.
292
+ Without it there is no per-device value to join with property or status sources.
293
+ `test-rule` lists, per device, the values it got and why a device was skipped.
294
+ - Conditions: `{ "field": "<source name>.<field>", "comparison": "above" | "below" | "equal" |
295
+ "not_equal" | "inside" | "outside" | "present" | "absent", "value": "<string>" }` (`min` /
296
+ `max` for inside / outside), joined with `"operator": "and" | "or"`. **Values are strings**
297
+ (`"30"`, not `30`). A field can be an expression: `ts() - status.connection.ts` is the time
298
+ since the last connect / disconnect. A value (or `min` / `max`) can be an expression too, over
299
+ any source of the same device: `"limits.value.max_depth_m"`, `"limits.value.max_depth_m * 0.9"`.
300
+ Booleans compare as 1 / 0. A comparison on a field the device does not have is false
301
+ (`not_equal` included), so `zone1 above 5` or `zone2 above 5` works on devices that only have
302
+ `zone1`; test a field with `present` / `absent`. A device is skipped when it has none of the
303
+ fields the conditions read, or misses a whole source, unless `present` / `absent` are used.
304
+ Examples:
305
+ - threshold: `{ "field": "climate.temperature", "comparison": "above", "value": "30" }`;
306
+ - per-device threshold: a bucket source `well` (`group_by: ["device"]`) plus a
307
+ `device_property` source `limits` (`property: "limits"`, `fields: ["value.max_depth_m"]`),
308
+ condition `well.depth_m` `above` `"limits.value.max_depth_m"`. Put the default limit on the
309
+ product or asset group and override it on the devices that need another: one rule for all;
310
+ - offline for 10 min: `status.connection.active` `equal` `"0"` and
311
+ `ts() - status.connection.ts` `above` `"600000"` (a device that never connected has
312
+ `connection.ts` 0, so it counts as offline from the start); the same with a `timespan`
313
+ confirmation of 10 minutes on `connection.active` `equal` `"0"` alone also works;
314
+ - no data for 10 min: a `device_status` source plus a bucket source with
315
+ `aggregation: { period: "10m", type: "count" }`, condition `<bucket source>.<field>`
316
+ `absent` (a device whose source returns nothing is otherwise skipped).
317
+ - `activation` and `normalization` each have `conditions` and a `confirmation`: `immediate`,
318
+ `sequence` (N checks in a row) or `timespan` (true for a duration). Without normalization
319
+ conditions, the alarm clears when activation is false.
320
+ - It creates one **alarm instance** per device that triggers. List them with
321
+ `GET …/alarms/instances?alarm.rule=<rule>&origin.id=<device>&state=1`: like in any resource
322
+ list, a field of the listed documents (nested ones with dots) filters by exact value (numbers
323
+ as numbers, `true` / `false` also as booleans: `?connection.active=true` for connected devices),
324
+ and a repeated parameter matches any of its values (nested fields and booleans since Thinger
325
+ 7.5). Instance `state` values:
326
+ 0 none, 1 activated, 2 acknowledged, 3 latched, 4 shelved, 5 cleared. `severity` values:
327
+ 0 none, 1 low, 2 medium, 3 high, 4 critical.
328
+ - Notifications on activation, normalization and reminders call **endpoints**:
329
+ `[{ "type": "endpoint", "id": "<endpoint>" }]`. The endpoint receives the alarm instance
330
+ (`name`, `severity`, `state`, `origin.id` = device, `evaluation.last_values`, `notification.type`…).
331
+ - Test a rule, a single source or a notification before saving with
332
+ `POST …/alarms/rules/<r>/test-rule`, `/test-source` and `/test-notification`. `test-rule` and
333
+ `test-source` work before the rule exists (any new id; since Thinger 7.5); `test-source` takes a `check_interval`
334
+ for bucket sources without aggregation (default: the rule's, or 1 minute).
335
+
336
+ ## Endpoints, storages, tokens
337
+
338
+ - **Endpoint**: an outgoing action (`email`, `http_request`, templates for third-party
339
+ services). Called from the API, devices, products and alarms.
340
+ - **File storage**: files (product code, firmware, web apps). A `public` storage serves its
341
+ files without authentication: that is how url dashboards are hosted.
342
+ - **Access token**: a long-lived credential with allow/deny permissions per resource type, id
343
+ and action. It can be limited to projects, in which case it must be used with `?project=`.
344
+
345
+ ## Dashboards
346
+
347
+ - **Build dashboards as SDK apps**: url dashboards (`source: "url"`) are web apps (Angular,
348
+ React, Vue…) built with the dashboard SDK, the npm package `@thinger-io/dashboard-sdk`
349
+ (reference in its README), uploaded to a file storage and rendered in an iframe. They read
350
+ devices, buckets, properties, alarms and live events, and can act on devices.
351
+ - **Widget dashboards** (made in the console editor) are legacy: do not create new ones.
352
+ - A dashboard lives either in the dashboards collection (standalone, no context) or as the
353
+ `dashboard` property of a device, product, asset type, asset group or project. In the
354
+ second case it opens from that resource's page, and the SDK gets its id in `context`. A
355
+ product dashboard shows up on each of its devices, so one page serves the whole fleet.
356
+
357
+ ## Events
358
+
359
+ Real-time notifications over `/v2/users/<u>/events` (the SDK's `events.subscribe`). Examples:
360
+ `device_state_change` (connected, disconnected…), `device_property_update`, `bucket_write`,
361
+ `device_resource_stream`, `device_location_update`, `device_geofence_event` (see Geofences),
362
+ `alarm_instance_activate` / `normalize`, `endpoint_call`, and
363
+ `<type>_create|update|delete` for any resource. Access tokens and project members can only
364
+ subscribe to `device_property_update`, `bucket_write` and `alarm_instance_*`.
365
+
366
+ ## Choosing where data comes from
367
+
368
+ | Need | Use |
369
+ |---|---|
370
+ | Current value of a sensor, live | device resource stream (`devices.streamResource`) |
371
+ | Read the device on demand | device resource (`devices.getResource`) |
372
+ | Act on a device | device resource call, or product API (`devices.callResource`, `products.call`) |
373
+ | History, charts, aggregates | bucket (`buckets.read` / `subscribe`; `device: true` for product buckets) |
374
+ | Configuration or state that survives offline | property (`devices.getProperty`, `properties.*`) |
375
+ | Is it online, and since when | `devices.get` → `connection`; `devices.subscribeState` |
376
+ | Where it is | `devices.locations` (maps), the `location` property |
377
+ | Entries into and exits from zones | the `geofences` property and `device_geofence_event` (see Geofences) |
378
+ | Notify someone | endpoint (`endpoints.call`), or an alarm rule with notifications |
379
+
380
+ ## Recipes
381
+
382
+ - **Same dashboard for every device of a product**: set it as the product's `dashboard`
383
+ property. Read `Thinger.context.device`, and use product buckets with `device: true`.
384
+ - **Fleet overview with drill-down**: a project or standalone dashboard that lists devices
385
+ (`devices.list({ product })`) and routes to a detail view. One app, several entry points.
386
+ - **Per-site view**: put devices in an asset group per site, and filter with
387
+ `asset_group` / `devices.locations({ group })`.
388
+ - **Data from many devices in one chart**: one product bucket shared by all devices (tagged
389
+ with `device` automatically); read it once and split the records by their `device` field.