signalk-cli 1.2.0__tar.gz → 2.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.
- {signalk_cli-1.2.0 → signalk_cli-2.1.0}/PKG-INFO +153 -8
- {signalk_cli-1.2.0 → signalk_cli-2.1.0}/README.md +150 -5
- signalk_cli-2.1.0/pyproject.toml +117 -0
- signalk_cli-1.2.0/pyproject.toml → signalk_cli-2.1.0/pyproject.toml.orig +8 -6
- {signalk_cli-1.2.0 → signalk_cli-2.1.0}/src/signalk_cli/__main__.py +2 -0
- {signalk_cli-1.2.0 → signalk_cli-2.1.0}/src/signalk_cli/history/cli.py +8 -50
- {signalk_cli-1.2.0 → signalk_cli-2.1.0}/src/signalk_cli/history/history_api.py +33 -83
- {signalk_cli-1.2.0 → signalk_cli-2.1.0}/src/signalk_cli/history/output.py +5 -3
- signalk_cli-2.1.0/src/signalk_cli/net.py +131 -0
- signalk_cli-2.1.0/src/signalk_cli/stream/__init__.py +21 -0
- signalk_cli-2.1.0/src/signalk_cli/stream/__main__.py +3 -0
- signalk_cli-2.1.0/src/signalk_cli/stream/cli.py +315 -0
- signalk_cli-2.1.0/src/signalk_cli/stream/output.py +108 -0
- signalk_cli-2.1.0/src/signalk_cli/stream/stream_api.py +106 -0
- {signalk_cli-1.2.0 → signalk_cli-2.1.0}/src/signalk_cli/__init__.py +0 -0
- {signalk_cli-1.2.0 → signalk_cli-2.1.0}/src/signalk_cli/history/__init__.py +0 -0
- {signalk_cli-1.2.0 → signalk_cli-2.1.0}/src/signalk_cli/history/__main__.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: signalk-cli
|
|
3
|
-
Version: 1.
|
|
4
|
-
Summary: Query
|
|
3
|
+
Version: 2.1.0
|
|
4
|
+
Summary: Query SignalK v2 APIs and export data as CSV, JSON or Apache Arrow Feather
|
|
5
5
|
Keywords: signalk,sailing,marine,nmea,boating
|
|
6
6
|
Author: jey burrows
|
|
7
7
|
Author-email: jey burrows <jrb@rhizomatics.org.uk>
|
|
@@ -15,7 +15,7 @@ Classifier: Programming Language :: Python
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.13
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.14
|
|
17
17
|
Requires-Dist: click>=8.3.3
|
|
18
|
-
Requires-Dist: niquests>=2.28
|
|
18
|
+
Requires-Dist: niquests[ws]>=2.28
|
|
19
19
|
Requires-Dist: zeroconf>=0.149.16,<0.150.0
|
|
20
20
|
Requires-Dist: pyarrow>=17.0 ; extra == 'feather'
|
|
21
21
|
Requires-Python: >=3.13
|
|
@@ -28,14 +28,25 @@ Description-Content-Type: text/markdown
|
|
|
28
28
|
|
|
29
29
|
# SignalK CLI
|
|
30
30
|
|
|
31
|
-
Query and explore SignalK APIs
|
|
31
|
+
Query and explore NMEA and other boat data from SignalK APIs using the command line, and export data as CSV, Apache Arrow Feather, or JSON.
|
|
32
32
|
|
|
33
33
|
APIs supported:
|
|
34
34
|
|
|
35
|
-
* [SignalK v2 History API](https://signalk.org/https://demo.signalk.org/documentation/Developing/REST_APIs/History_API.html)
|
|
35
|
+
* [SignalK v2 History API](https://signalk.org/https://demo.signalk.org/documentation/Developing/REST_APIs/History_API.html). Commands available:
|
|
36
|
+
- list-paths
|
|
37
|
+
- list-providers
|
|
38
|
+
- list-contexts
|
|
39
|
+
- query
|
|
40
|
+
- cardinality
|
|
41
|
+
* [SignalK v1 Streaming API](https://signalk.org/specification/1.8.2/doc/streaming_api.html). Commands available:
|
|
42
|
+
- deltas
|
|
36
43
|
|
|
37
44
|
## Installation
|
|
38
45
|
|
|
46
|
+
`signalk-cli` is published to PyPi at https://pypi.org/project/signalk-cli/
|
|
47
|
+
|
|
48
|
+
Python is required to run this, version 3.13 or above. [uv](https://docs.astral.sh/uv/) is the recommended way to install the package ( and can install Python ) but is not required.
|
|
49
|
+
|
|
39
50
|
### PyPi
|
|
40
51
|
|
|
41
52
|
```pip install signalk-cli``` or ```uv pip install signalk-cli```
|
|
@@ -60,14 +71,17 @@ Use `uv` to run without installing the module permanently, for example:
|
|
|
60
71
|
uv run --with signalk-cli signalk_cli.history list-providers
|
|
61
72
|
```
|
|
62
73
|
|
|
74
|
+
```bash
|
|
75
|
+
uv run --with signalk-cli signalk_cli.stream deltas navigation.position --follow
|
|
76
|
+
```
|
|
77
|
+
|
|
63
78
|
## Running
|
|
64
79
|
|
|
65
|
-
Run via `python -m signalk_cli.history <command>`.
|
|
80
|
+
Run via `python -m signalk_cli.history <command>` or `python -m signalk_cli.stream <command>` or without installing the module with `uv run --with signalk-cli signalk_cli.history <command>`.
|
|
66
81
|
|
|
67
82
|
## Determining SignalK host name
|
|
68
83
|
|
|
69
|
-
If no host name is set as an argument, the CLI will look for a `SIGNALK_HOST` environment variable, and failing that attempt to automatically discover
|
|
70
|
-
the host using mDNS (aka Bonjour) and locally cached (see [Default Caching](#default-caching)).
|
|
84
|
+
If no host name is set as an argument, the CLI will look for a `SIGNALK_HOST` environment variable, and failing that attempt to automatically discover the host using mDNS (aka Bonjour) and locally cached (see [Default Caching](#default-caching)).
|
|
71
85
|
|
|
72
86
|
```bash
|
|
73
87
|
export SIGNALK_HOST=192.168.6.99 # http:// is added automatically if omitted
|
|
@@ -395,6 +409,137 @@ python -m signalk_cli.history list-contexts --host 10.36.10.21 --format raw --ba
|
|
|
395
409
|
|
|
396
410
|
---
|
|
397
411
|
|
|
412
|
+
## SignalK v1 Streaming API
|
|
413
|
+
|
|
414
|
+
The `signalk_cli.stream` module connects to a running SignalK server's live delta
|
|
415
|
+
WebSocket feed (as opposed to `signalk_cli.history`, which queries recorded history).
|
|
416
|
+
|
|
417
|
+
### `deltas`
|
|
418
|
+
|
|
419
|
+
Stream live delta updates from the SignalK v1 Streaming API.
|
|
420
|
+
|
|
421
|
+
```
|
|
422
|
+
python -m signalk_cli.stream deltas [OPTIONS] [PATH...]
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
By default, prints the next delta message and exits — useful for a quick check.
|
|
426
|
+
Use `--follow` to keep tailing until interrupted with Ctrl-C, optionally capped
|
|
427
|
+
with `--count`.
|
|
428
|
+
|
|
429
|
+
**PATH** arguments are sent verbatim to the server as an explicit subscription,
|
|
430
|
+
one per path. They may be literal SignalK paths (e.g. `navigation.speedOverGround`)
|
|
431
|
+
or use the SignalK subscription wildcard `*`, matched server-side per the
|
|
432
|
+
[Subscription Protocol](https://signalk.org/specification/1.8.2/doc/subscription_protocol.html)
|
|
433
|
+
— unlike `history`'s PATH patterns, which the client resolves by matching
|
|
434
|
+
against the server's enumerated `/paths` list:
|
|
435
|
+
|
|
436
|
+
- `*` at the end of a path matches any suffix, e.g. `navigation.*`
|
|
437
|
+
- `*` as a middle segment matches any single segment there, e.g. `propulsion.*.oilTemperature`
|
|
438
|
+
- a bare `*` subscribes to every path in the context
|
|
439
|
+
|
|
440
|
+
Quote wildcarded paths (e.g. `'navigation.*'`) so the shell doesn't expand them
|
|
441
|
+
against local filenames first.
|
|
442
|
+
|
|
443
|
+
`deltas` always sends its own explicit subscribe message for `--context`
|
|
444
|
+
(default `vessels.self`), covering PATH arguments if given, otherwise every
|
|
445
|
+
path (`*`). `--context` also accepts the SignalK wildcard: `'*'` or
|
|
446
|
+
`'vessels.*'` subscribes to every vessel, not just your own. `--policy`,
|
|
447
|
+
`--period`, and `--min-period` control that subscription per the
|
|
448
|
+
[Subscription Protocol](https://signalk.org/specification/1.8.2/doc/subscription_protocol.html)'s
|
|
449
|
+
`policy`/`period`/`minPeriod` fields:
|
|
450
|
+
|
|
451
|
+
- **`--policy`** (`instant` / `ideal` / `fixed`, default `ideal`): `instant` sends every change (throttled by `--min-period`); `ideal` behaves like `instant` but resends the last value if nothing changes within `--period`; `fixed` always sends the last known value every `--period`, regardless of changes.
|
|
452
|
+
- **`--period`** (seconds, default `60`): the resend interval for `ideal`/`fixed`. Converted to milliseconds on the wire.
|
|
453
|
+
- **`--min-period`** (seconds): fastest allowed transmission rate, only meaningful with `--policy instant`.
|
|
454
|
+
|
|
455
|
+
The protocol also defines a per-path `format` field (`delta`/`full`), but
|
|
456
|
+
this CLI doesn't expose it: signalk-server rejects `full` outright ("Only
|
|
457
|
+
delta format supported, using it") and always sends delta messages
|
|
458
|
+
regardless, so the choice would be misleading.
|
|
459
|
+
|
|
460
|
+
**`--subscribe` is a separate, easily-confused mechanism**: it's the
|
|
461
|
+
connection-level `subscribe` query parameter (`none`/`self`/`all`), which
|
|
462
|
+
controls whether the server *additionally* auto-subscribes the connection
|
|
463
|
+
at **its own** default policy/period — you don't get to choose the rate.
|
|
464
|
+
This CLI defaults it to `none`, diverging from the SignalK spec's own
|
|
465
|
+
default of `self`, because `deltas` always sends its own explicit `--context`
|
|
466
|
+
subscription anyway; leaving the connection-level default at `self` would
|
|
467
|
+
just double up on updates for your own vessel.
|
|
468
|
+
|
|
469
|
+
So there are two different ways to see every vessel, and they're not
|
|
470
|
+
interchangeable:
|
|
471
|
+
|
|
472
|
+
| Want | Use | Policy/period used |
|
|
473
|
+
|---|---|---|
|
|
474
|
+
| Just your own vessel | (defaults) | Yours (`--policy`/`--period`) |
|
|
475
|
+
| Every vessel, at your chosen rate | `--context '*'` | Yours (`--policy`/`--period`) |
|
|
476
|
+
| Every vessel, at whatever rate the server defaults to | `--subscribe all` | The server's own default |
|
|
477
|
+
|
|
478
|
+
#### Options
|
|
479
|
+
|
|
480
|
+
| Option | Default | Description |
|
|
481
|
+
|---|---|---|
|
|
482
|
+
| `--host` | `$SIGNALK_HOST` | Server base URL. `http://` added if scheme omitted; converted to `ws://`/`wss://` for the stream connection. |
|
|
483
|
+
| `--no-cache` | — | Ignore the cached host |
|
|
484
|
+
| `-c, --context TEXT` | `vessels.self` | SignalK context |
|
|
485
|
+
| `--subscribe [none\|self\|all]` | `none` | Connection-level subscribe policy (SignalK's own `subscribe` query parameter); see above |
|
|
486
|
+
| `--policy [instant\|ideal\|fixed]` | `ideal` | Per-path subscribe `policy` field; see above |
|
|
487
|
+
| `--period SECONDS` | `60` | Per-path subscribe `period` field, in seconds (converted to ms) |
|
|
488
|
+
| `--min-period SECONDS` | — | Per-path subscribe `minPeriod` field, in seconds (converted to ms); only meaningful with `--policy instant` |
|
|
489
|
+
| `--format [csv\|json\|raw\|feather]` | from extension, else csv | Output format. `json` is JSON Lines (one row object per line, suitable for a live stream). `raw` is the exact delta message text, one per line. `feather` requires `pip install 'signalk-cli[feather]'` and `--output` (cannot stream to stdout). |
|
|
490
|
+
| `--no-header` | — | Suppress the CSV header row |
|
|
491
|
+
| `--include-meta` | — | Also emit rows for `meta` entries (units, description, zones, etc.), not just `values`. Adds a `kind` column (`value`/`meta`) to csv/json/feather output. Ignored for `--format raw`, which always includes meta as-is. |
|
|
492
|
+
| `-o, --output [FILE]` | stdout | Write to a file. Omit the filename (`--output` alone) to auto-name as `signalk-stream-<server>-<timestamp>.<ext>`. Required for `--format feather`. |
|
|
493
|
+
| `-f, --follow` | — | Keep streaming until interrupted (Ctrl-C) or `--count` is reached. Without this, print the next message then exit. |
|
|
494
|
+
| `-n, --count N` | 1 without `--follow`, unlimited with it | Number of delta messages to output |
|
|
495
|
+
| `--bare` | — | Print to stdout with **no informational messages**. Ideal for piping to other tools. |
|
|
496
|
+
|
|
497
|
+
#### Output formats
|
|
498
|
+
|
|
499
|
+
**csv** / **json**: `timestamp, context, source, path, value` — one row per `path`/`value` pair in each delta's updates, written incrementally as messages arrive. Structured values (e.g. `navigation.position`) are JSON-encoded in the `value` column. With `--include-meta`, a `kind` column (`value`/`meta`) is inserted before `value`, and `meta` entries (units, description, zones, etc. — also JSON-encoded) are included as rows too; without it, `meta` entries are silently dropped from csv/json/feather (they're still present in `raw`).
|
|
500
|
+
|
|
501
|
+
**raw**: the exact delta message JSON as received from the server, one message per line.
|
|
502
|
+
|
|
503
|
+
**feather**: Apache Arrow Feather binary format, same columns as csv/json. Requires `pip install 'signalk-cli[feather]'`. Unlike the other formats, rows are buffered in memory across all received messages and written once the session ends (`--count` reached, or Ctrl-C with `--follow`) — cannot be streamed to stdout.
|
|
504
|
+
|
|
505
|
+
#### Examples
|
|
506
|
+
|
|
507
|
+
```bash
|
|
508
|
+
# Next update for one path, then exit
|
|
509
|
+
python -m signalk_cli.stream deltas --host 10.36.10.21 navigation.speedOverGround
|
|
510
|
+
|
|
511
|
+
# Tail all navigation updates until Ctrl-C
|
|
512
|
+
python -m signalk_cli.stream deltas --host 10.36.10.21 --follow 'navigation.*'
|
|
513
|
+
|
|
514
|
+
# Tail oil temperature across every engine (mid-path wildcard)
|
|
515
|
+
python -m signalk_cli.stream deltas --host 10.36.10.21 --follow 'propulsion.*.oilTemperature'
|
|
516
|
+
|
|
517
|
+
# Next 20 messages across all subscribed paths, as JSON Lines
|
|
518
|
+
python -m signalk_cli.stream deltas --host 10.36.10.21 --format json --count 20
|
|
519
|
+
|
|
520
|
+
# Tail two specific paths, piping raw deltas to another tool
|
|
521
|
+
python -m signalk_cli.stream deltas --host 10.36.10.21 --follow --format raw --bare \
|
|
522
|
+
navigation.speedOverGround navigation.courseOverGroundTrue
|
|
523
|
+
|
|
524
|
+
# Everything the server has (equivalent to ?subscribe=all), following
|
|
525
|
+
python -m signalk_cli.stream deltas --host 10.36.10.21 --subscribe all --follow
|
|
526
|
+
|
|
527
|
+
# Faster resend interval (5s instead of the 60s default)
|
|
528
|
+
python -m signalk_cli.stream deltas --host 10.36.10.21 --follow --period 5 'navigation.*'
|
|
529
|
+
|
|
530
|
+
# Send every change immediately, no more than 5 times/second
|
|
531
|
+
python -m signalk_cli.stream deltas --host 10.36.10.21 --follow \
|
|
532
|
+
--policy instant --min-period 0.2 navigation.speedOverGround
|
|
533
|
+
|
|
534
|
+
# Capture 500 messages to an auto-named Feather file
|
|
535
|
+
python -m signalk_cli.stream deltas --host 10.36.10.21 --count 500 --format feather --output 'navigation.*'
|
|
536
|
+
|
|
537
|
+
# Capture until Ctrl-C to a named Feather file
|
|
538
|
+
python -m signalk_cli.stream deltas --host 10.36.10.21 --follow -o capture.feather 'navigation.*'
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
---
|
|
542
|
+
|
|
398
543
|
## Time range
|
|
399
544
|
|
|
400
545
|
All commands that query data accept the same three time parameters. At least one of `--from` or `--duration` must be provided (or the tool supplies a one-hour default).
|
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
# SignalK CLI
|
|
2
2
|
|
|
3
|
-
Query and explore SignalK APIs
|
|
3
|
+
Query and explore NMEA and other boat data from SignalK APIs using the command line, and export data as CSV, Apache Arrow Feather, or JSON.
|
|
4
4
|
|
|
5
5
|
APIs supported:
|
|
6
6
|
|
|
7
|
-
* [SignalK v2 History API](https://signalk.org/https://demo.signalk.org/documentation/Developing/REST_APIs/History_API.html)
|
|
7
|
+
* [SignalK v2 History API](https://signalk.org/https://demo.signalk.org/documentation/Developing/REST_APIs/History_API.html). Commands available:
|
|
8
|
+
- list-paths
|
|
9
|
+
- list-providers
|
|
10
|
+
- list-contexts
|
|
11
|
+
- query
|
|
12
|
+
- cardinality
|
|
13
|
+
* [SignalK v1 Streaming API](https://signalk.org/specification/1.8.2/doc/streaming_api.html). Commands available:
|
|
14
|
+
- deltas
|
|
8
15
|
|
|
9
16
|
## Installation
|
|
10
17
|
|
|
18
|
+
`signalk-cli` is published to PyPi at https://pypi.org/project/signalk-cli/
|
|
19
|
+
|
|
20
|
+
Python is required to run this, version 3.13 or above. [uv](https://docs.astral.sh/uv/) is the recommended way to install the package ( and can install Python ) but is not required.
|
|
21
|
+
|
|
11
22
|
### PyPi
|
|
12
23
|
|
|
13
24
|
```pip install signalk-cli``` or ```uv pip install signalk-cli```
|
|
@@ -32,14 +43,17 @@ Use `uv` to run without installing the module permanently, for example:
|
|
|
32
43
|
uv run --with signalk-cli signalk_cli.history list-providers
|
|
33
44
|
```
|
|
34
45
|
|
|
46
|
+
```bash
|
|
47
|
+
uv run --with signalk-cli signalk_cli.stream deltas navigation.position --follow
|
|
48
|
+
```
|
|
49
|
+
|
|
35
50
|
## Running
|
|
36
51
|
|
|
37
|
-
Run via `python -m signalk_cli.history <command>`.
|
|
52
|
+
Run via `python -m signalk_cli.history <command>` or `python -m signalk_cli.stream <command>` or without installing the module with `uv run --with signalk-cli signalk_cli.history <command>`.
|
|
38
53
|
|
|
39
54
|
## Determining SignalK host name
|
|
40
55
|
|
|
41
|
-
If no host name is set as an argument, the CLI will look for a `SIGNALK_HOST` environment variable, and failing that attempt to automatically discover
|
|
42
|
-
the host using mDNS (aka Bonjour) and locally cached (see [Default Caching](#default-caching)).
|
|
56
|
+
If no host name is set as an argument, the CLI will look for a `SIGNALK_HOST` environment variable, and failing that attempt to automatically discover the host using mDNS (aka Bonjour) and locally cached (see [Default Caching](#default-caching)).
|
|
43
57
|
|
|
44
58
|
```bash
|
|
45
59
|
export SIGNALK_HOST=192.168.6.99 # http:// is added automatically if omitted
|
|
@@ -367,6 +381,137 @@ python -m signalk_cli.history list-contexts --host 10.36.10.21 --format raw --ba
|
|
|
367
381
|
|
|
368
382
|
---
|
|
369
383
|
|
|
384
|
+
## SignalK v1 Streaming API
|
|
385
|
+
|
|
386
|
+
The `signalk_cli.stream` module connects to a running SignalK server's live delta
|
|
387
|
+
WebSocket feed (as opposed to `signalk_cli.history`, which queries recorded history).
|
|
388
|
+
|
|
389
|
+
### `deltas`
|
|
390
|
+
|
|
391
|
+
Stream live delta updates from the SignalK v1 Streaming API.
|
|
392
|
+
|
|
393
|
+
```
|
|
394
|
+
python -m signalk_cli.stream deltas [OPTIONS] [PATH...]
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
By default, prints the next delta message and exits — useful for a quick check.
|
|
398
|
+
Use `--follow` to keep tailing until interrupted with Ctrl-C, optionally capped
|
|
399
|
+
with `--count`.
|
|
400
|
+
|
|
401
|
+
**PATH** arguments are sent verbatim to the server as an explicit subscription,
|
|
402
|
+
one per path. They may be literal SignalK paths (e.g. `navigation.speedOverGround`)
|
|
403
|
+
or use the SignalK subscription wildcard `*`, matched server-side per the
|
|
404
|
+
[Subscription Protocol](https://signalk.org/specification/1.8.2/doc/subscription_protocol.html)
|
|
405
|
+
— unlike `history`'s PATH patterns, which the client resolves by matching
|
|
406
|
+
against the server's enumerated `/paths` list:
|
|
407
|
+
|
|
408
|
+
- `*` at the end of a path matches any suffix, e.g. `navigation.*`
|
|
409
|
+
- `*` as a middle segment matches any single segment there, e.g. `propulsion.*.oilTemperature`
|
|
410
|
+
- a bare `*` subscribes to every path in the context
|
|
411
|
+
|
|
412
|
+
Quote wildcarded paths (e.g. `'navigation.*'`) so the shell doesn't expand them
|
|
413
|
+
against local filenames first.
|
|
414
|
+
|
|
415
|
+
`deltas` always sends its own explicit subscribe message for `--context`
|
|
416
|
+
(default `vessels.self`), covering PATH arguments if given, otherwise every
|
|
417
|
+
path (`*`). `--context` also accepts the SignalK wildcard: `'*'` or
|
|
418
|
+
`'vessels.*'` subscribes to every vessel, not just your own. `--policy`,
|
|
419
|
+
`--period`, and `--min-period` control that subscription per the
|
|
420
|
+
[Subscription Protocol](https://signalk.org/specification/1.8.2/doc/subscription_protocol.html)'s
|
|
421
|
+
`policy`/`period`/`minPeriod` fields:
|
|
422
|
+
|
|
423
|
+
- **`--policy`** (`instant` / `ideal` / `fixed`, default `ideal`): `instant` sends every change (throttled by `--min-period`); `ideal` behaves like `instant` but resends the last value if nothing changes within `--period`; `fixed` always sends the last known value every `--period`, regardless of changes.
|
|
424
|
+
- **`--period`** (seconds, default `60`): the resend interval for `ideal`/`fixed`. Converted to milliseconds on the wire.
|
|
425
|
+
- **`--min-period`** (seconds): fastest allowed transmission rate, only meaningful with `--policy instant`.
|
|
426
|
+
|
|
427
|
+
The protocol also defines a per-path `format` field (`delta`/`full`), but
|
|
428
|
+
this CLI doesn't expose it: signalk-server rejects `full` outright ("Only
|
|
429
|
+
delta format supported, using it") and always sends delta messages
|
|
430
|
+
regardless, so the choice would be misleading.
|
|
431
|
+
|
|
432
|
+
**`--subscribe` is a separate, easily-confused mechanism**: it's the
|
|
433
|
+
connection-level `subscribe` query parameter (`none`/`self`/`all`), which
|
|
434
|
+
controls whether the server *additionally* auto-subscribes the connection
|
|
435
|
+
at **its own** default policy/period — you don't get to choose the rate.
|
|
436
|
+
This CLI defaults it to `none`, diverging from the SignalK spec's own
|
|
437
|
+
default of `self`, because `deltas` always sends its own explicit `--context`
|
|
438
|
+
subscription anyway; leaving the connection-level default at `self` would
|
|
439
|
+
just double up on updates for your own vessel.
|
|
440
|
+
|
|
441
|
+
So there are two different ways to see every vessel, and they're not
|
|
442
|
+
interchangeable:
|
|
443
|
+
|
|
444
|
+
| Want | Use | Policy/period used |
|
|
445
|
+
|---|---|---|
|
|
446
|
+
| Just your own vessel | (defaults) | Yours (`--policy`/`--period`) |
|
|
447
|
+
| Every vessel, at your chosen rate | `--context '*'` | Yours (`--policy`/`--period`) |
|
|
448
|
+
| Every vessel, at whatever rate the server defaults to | `--subscribe all` | The server's own default |
|
|
449
|
+
|
|
450
|
+
#### Options
|
|
451
|
+
|
|
452
|
+
| Option | Default | Description |
|
|
453
|
+
|---|---|---|
|
|
454
|
+
| `--host` | `$SIGNALK_HOST` | Server base URL. `http://` added if scheme omitted; converted to `ws://`/`wss://` for the stream connection. |
|
|
455
|
+
| `--no-cache` | — | Ignore the cached host |
|
|
456
|
+
| `-c, --context TEXT` | `vessels.self` | SignalK context |
|
|
457
|
+
| `--subscribe [none\|self\|all]` | `none` | Connection-level subscribe policy (SignalK's own `subscribe` query parameter); see above |
|
|
458
|
+
| `--policy [instant\|ideal\|fixed]` | `ideal` | Per-path subscribe `policy` field; see above |
|
|
459
|
+
| `--period SECONDS` | `60` | Per-path subscribe `period` field, in seconds (converted to ms) |
|
|
460
|
+
| `--min-period SECONDS` | — | Per-path subscribe `minPeriod` field, in seconds (converted to ms); only meaningful with `--policy instant` |
|
|
461
|
+
| `--format [csv\|json\|raw\|feather]` | from extension, else csv | Output format. `json` is JSON Lines (one row object per line, suitable for a live stream). `raw` is the exact delta message text, one per line. `feather` requires `pip install 'signalk-cli[feather]'` and `--output` (cannot stream to stdout). |
|
|
462
|
+
| `--no-header` | — | Suppress the CSV header row |
|
|
463
|
+
| `--include-meta` | — | Also emit rows for `meta` entries (units, description, zones, etc.), not just `values`. Adds a `kind` column (`value`/`meta`) to csv/json/feather output. Ignored for `--format raw`, which always includes meta as-is. |
|
|
464
|
+
| `-o, --output [FILE]` | stdout | Write to a file. Omit the filename (`--output` alone) to auto-name as `signalk-stream-<server>-<timestamp>.<ext>`. Required for `--format feather`. |
|
|
465
|
+
| `-f, --follow` | — | Keep streaming until interrupted (Ctrl-C) or `--count` is reached. Without this, print the next message then exit. |
|
|
466
|
+
| `-n, --count N` | 1 without `--follow`, unlimited with it | Number of delta messages to output |
|
|
467
|
+
| `--bare` | — | Print to stdout with **no informational messages**. Ideal for piping to other tools. |
|
|
468
|
+
|
|
469
|
+
#### Output formats
|
|
470
|
+
|
|
471
|
+
**csv** / **json**: `timestamp, context, source, path, value` — one row per `path`/`value` pair in each delta's updates, written incrementally as messages arrive. Structured values (e.g. `navigation.position`) are JSON-encoded in the `value` column. With `--include-meta`, a `kind` column (`value`/`meta`) is inserted before `value`, and `meta` entries (units, description, zones, etc. — also JSON-encoded) are included as rows too; without it, `meta` entries are silently dropped from csv/json/feather (they're still present in `raw`).
|
|
472
|
+
|
|
473
|
+
**raw**: the exact delta message JSON as received from the server, one message per line.
|
|
474
|
+
|
|
475
|
+
**feather**: Apache Arrow Feather binary format, same columns as csv/json. Requires `pip install 'signalk-cli[feather]'`. Unlike the other formats, rows are buffered in memory across all received messages and written once the session ends (`--count` reached, or Ctrl-C with `--follow`) — cannot be streamed to stdout.
|
|
476
|
+
|
|
477
|
+
#### Examples
|
|
478
|
+
|
|
479
|
+
```bash
|
|
480
|
+
# Next update for one path, then exit
|
|
481
|
+
python -m signalk_cli.stream deltas --host 10.36.10.21 navigation.speedOverGround
|
|
482
|
+
|
|
483
|
+
# Tail all navigation updates until Ctrl-C
|
|
484
|
+
python -m signalk_cli.stream deltas --host 10.36.10.21 --follow 'navigation.*'
|
|
485
|
+
|
|
486
|
+
# Tail oil temperature across every engine (mid-path wildcard)
|
|
487
|
+
python -m signalk_cli.stream deltas --host 10.36.10.21 --follow 'propulsion.*.oilTemperature'
|
|
488
|
+
|
|
489
|
+
# Next 20 messages across all subscribed paths, as JSON Lines
|
|
490
|
+
python -m signalk_cli.stream deltas --host 10.36.10.21 --format json --count 20
|
|
491
|
+
|
|
492
|
+
# Tail two specific paths, piping raw deltas to another tool
|
|
493
|
+
python -m signalk_cli.stream deltas --host 10.36.10.21 --follow --format raw --bare \
|
|
494
|
+
navigation.speedOverGround navigation.courseOverGroundTrue
|
|
495
|
+
|
|
496
|
+
# Everything the server has (equivalent to ?subscribe=all), following
|
|
497
|
+
python -m signalk_cli.stream deltas --host 10.36.10.21 --subscribe all --follow
|
|
498
|
+
|
|
499
|
+
# Faster resend interval (5s instead of the 60s default)
|
|
500
|
+
python -m signalk_cli.stream deltas --host 10.36.10.21 --follow --period 5 'navigation.*'
|
|
501
|
+
|
|
502
|
+
# Send every change immediately, no more than 5 times/second
|
|
503
|
+
python -m signalk_cli.stream deltas --host 10.36.10.21 --follow \
|
|
504
|
+
--policy instant --min-period 0.2 navigation.speedOverGround
|
|
505
|
+
|
|
506
|
+
# Capture 500 messages to an auto-named Feather file
|
|
507
|
+
python -m signalk_cli.stream deltas --host 10.36.10.21 --count 500 --format feather --output 'navigation.*'
|
|
508
|
+
|
|
509
|
+
# Capture until Ctrl-C to a named Feather file
|
|
510
|
+
python -m signalk_cli.stream deltas --host 10.36.10.21 --follow -o capture.feather 'navigation.*'
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
---
|
|
514
|
+
|
|
370
515
|
## Time range
|
|
371
516
|
|
|
372
517
|
All commands that query data accept the same three time parameters. At least one of `--from` or `--duration` must be provided (or the tool supplies a one-hour default).
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "signalk-cli"
|
|
3
|
+
version = "2.1.0"
|
|
4
|
+
description = "Query SignalK v2 APIs and export data as CSV, JSON or Apache Arrow Feather"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.13"
|
|
7
|
+
license = "Apache-2.0"
|
|
8
|
+
keywords = [
|
|
9
|
+
"signalk",
|
|
10
|
+
"sailing",
|
|
11
|
+
"marine",
|
|
12
|
+
"nmea",
|
|
13
|
+
"boating",
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 4 - Beta",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Natural Language :: English",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Topic :: Scientific/Engineering",
|
|
21
|
+
"Programming Language :: Python",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Programming Language :: Python :: 3.14",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"click>=8.3.3",
|
|
27
|
+
"niquests[ws]>=2.28",
|
|
28
|
+
"zeroconf>=0.149.16,<0.150.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[[project.authors]]
|
|
32
|
+
name = "jey burrows"
|
|
33
|
+
email = "jrb@rhizomatics.org.uk"
|
|
34
|
+
|
|
35
|
+
[project.optional-dependencies]
|
|
36
|
+
feather = ["pyarrow>=17.0"]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/rhizomatics/signalk-cli"
|
|
40
|
+
Repository = "https://github.com/rhizomatics/signalk-cli"
|
|
41
|
+
Issues = "https://github.com/rhizomatics/signalk-cli/issues"
|
|
42
|
+
Changelog = "https://github.com/rhizomatics/signalk-cli/blob/main/CHANGELOG.md"
|
|
43
|
+
|
|
44
|
+
[project.scripts]
|
|
45
|
+
"signalk_cli.history" = "signalk_cli.history.cli:cli"
|
|
46
|
+
"signalk_cli.stream" = "signalk_cli.stream.cli:cli"
|
|
47
|
+
|
|
48
|
+
[dependency-groups]
|
|
49
|
+
dev = [
|
|
50
|
+
"ruff>=0.16.0",
|
|
51
|
+
"pre-commit>=4.4.0",
|
|
52
|
+
"pytest>=9.0.0",
|
|
53
|
+
"pytest-mock>=3.15.0",
|
|
54
|
+
"pytest-cov>=7.0.0",
|
|
55
|
+
"pytest-xdist",
|
|
56
|
+
"mypy",
|
|
57
|
+
"coverage",
|
|
58
|
+
"icdiff",
|
|
59
|
+
"codespell",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
[tool.bandit]
|
|
63
|
+
exclude_dirs = ["tests"]
|
|
64
|
+
|
|
65
|
+
[tool.ruff.format]
|
|
66
|
+
quote-style = "double"
|
|
67
|
+
indent-style = "space"
|
|
68
|
+
skip-magic-trailing-comma = false
|
|
69
|
+
line-ending = "auto"
|
|
70
|
+
|
|
71
|
+
[tool.pytest.ini_options]
|
|
72
|
+
pythonpath = [
|
|
73
|
+
"src",
|
|
74
|
+
".",
|
|
75
|
+
]
|
|
76
|
+
asyncio_mode = "auto"
|
|
77
|
+
testpaths = ["tests"]
|
|
78
|
+
norecursedirs = [".git"]
|
|
79
|
+
|
|
80
|
+
[tool.coverage.run]
|
|
81
|
+
branch = true
|
|
82
|
+
source = ["src"]
|
|
83
|
+
|
|
84
|
+
[tool.coverage.report]
|
|
85
|
+
fail_under = 0
|
|
86
|
+
|
|
87
|
+
[tool.coverage.xml]
|
|
88
|
+
output = "cov.xml"
|
|
89
|
+
|
|
90
|
+
[tool.coverage.html]
|
|
91
|
+
directory = "htmlcov"
|
|
92
|
+
|
|
93
|
+
[tool.codespell]
|
|
94
|
+
skip = ".venv,.git,htmlcov,site,./.*,*.csv,*.json,README.*.md,support"
|
|
95
|
+
count = true
|
|
96
|
+
quiet-level = 2
|
|
97
|
+
ignore-words-list = "hass,referer,uint"
|
|
98
|
+
|
|
99
|
+
[tool.mypy]
|
|
100
|
+
mypy_path = ["./src"]
|
|
101
|
+
|
|
102
|
+
[tool.uv]
|
|
103
|
+
compile-bytecode = true
|
|
104
|
+
managed = true
|
|
105
|
+
exclude-newer = "7 days"
|
|
106
|
+
add-bounds = "major"
|
|
107
|
+
|
|
108
|
+
[tool.uv.exclude-newer-package]
|
|
109
|
+
uv_build = "2 days"
|
|
110
|
+
|
|
111
|
+
[tool.uv.build-backend]
|
|
112
|
+
module-root = "src"
|
|
113
|
+
module-name = "signalk_cli"
|
|
114
|
+
|
|
115
|
+
[build-system]
|
|
116
|
+
requires = ["uv_build>=0.12.0,<0.13.0"]
|
|
117
|
+
build-backend = "uv_build"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "signalk-cli"
|
|
3
|
-
version = "1.
|
|
4
|
-
description = "Query
|
|
3
|
+
version = "2.1.0"
|
|
4
|
+
description = "Query SignalK v2 APIs and export data as CSV, JSON or Apache Arrow Feather"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.13"
|
|
7
7
|
license="Apache-2.0"
|
|
@@ -23,7 +23,7 @@ classifiers = [
|
|
|
23
23
|
|
|
24
24
|
dependencies = [
|
|
25
25
|
"click>=8.3.3",
|
|
26
|
-
"niquests>=2.28",
|
|
26
|
+
"niquests[ws]>=2.28",
|
|
27
27
|
"zeroconf>=0.149.16,<0.150.0",
|
|
28
28
|
]
|
|
29
29
|
|
|
@@ -31,7 +31,7 @@ dependencies = [
|
|
|
31
31
|
feather = ["pyarrow>=17.0"]
|
|
32
32
|
[dependency-groups]
|
|
33
33
|
dev = [
|
|
34
|
-
"ruff>=0.
|
|
34
|
+
"ruff>=0.16.0",
|
|
35
35
|
"pre-commit>=4.4.0",
|
|
36
36
|
"pytest>=9.0.0",
|
|
37
37
|
"pytest-mock>=3.15.0",
|
|
@@ -101,11 +101,12 @@ mypy_path=["./src"]
|
|
|
101
101
|
[tool.uv]
|
|
102
102
|
compile-bytecode = true
|
|
103
103
|
managed = true
|
|
104
|
-
exclude-newer = "
|
|
104
|
+
exclude-newer = "7 days"
|
|
105
|
+
exclude-newer-package = {"uv_build"="2 days"}
|
|
105
106
|
add-bounds = "major"
|
|
106
107
|
|
|
107
108
|
[build-system]
|
|
108
|
-
requires = ["uv_build>=0.
|
|
109
|
+
requires = ["uv_build>=0.12.0,<0.13.0"]
|
|
109
110
|
build-backend = "uv_build"
|
|
110
111
|
|
|
111
112
|
|
|
@@ -115,3 +116,4 @@ module-name = "signalk_cli"
|
|
|
115
116
|
|
|
116
117
|
[project.scripts]
|
|
117
118
|
"signalk_cli.history" = "signalk_cli.history.cli:cli"
|
|
119
|
+
"signalk_cli.stream" = "signalk_cli.stream.cli:cli"
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
print("Supported SignalK APIs:")
|
|
4
4
|
print(" signalk_cli.history — SignalK v2 History API")
|
|
5
|
+
print(" signalk_cli.stream — SignalK v1 Streaming (delta) API")
|
|
5
6
|
print()
|
|
6
7
|
print("Usage: python -m signalk_cli.<api> [COMMAND] [OPTIONS]")
|
|
7
8
|
print(" python -m signalk_cli.history --help")
|
|
9
|
+
print(" python -m signalk_cli.stream --help")
|
|
@@ -1,35 +1,30 @@
|
|
|
1
1
|
"""Click CLI for the SignalK v2 History API."""
|
|
2
2
|
|
|
3
|
-
import contextlib
|
|
4
3
|
import csv
|
|
5
|
-
import io
|
|
6
4
|
import json
|
|
7
5
|
import re
|
|
8
6
|
import sys
|
|
9
|
-
from datetime import
|
|
7
|
+
from datetime import UTC, datetime
|
|
10
8
|
from pathlib import Path
|
|
11
9
|
from urllib.parse import urlparse
|
|
12
10
|
|
|
13
11
|
import click
|
|
14
12
|
import niquests
|
|
15
13
|
|
|
14
|
+
from ..net import bare_option, host_option, resolve_host, stderr_ctx
|
|
16
15
|
from .history_api import (
|
|
17
16
|
HISTORY_BASE,
|
|
18
17
|
api_error,
|
|
19
18
|
apply_time_default,
|
|
20
|
-
discover_host,
|
|
21
19
|
expand_paths,
|
|
22
20
|
fetch_server_paths,
|
|
23
|
-
get_cached_host,
|
|
24
21
|
normalise_duration,
|
|
25
|
-
normalise_host,
|
|
26
22
|
resolve_provider,
|
|
27
|
-
save_cached_host,
|
|
28
23
|
)
|
|
29
24
|
from .output import (
|
|
25
|
+
_POSITION_RE,
|
|
30
26
|
CARDINALITY_COLUMNS,
|
|
31
27
|
FEATHER_EXTENSIONS,
|
|
32
|
-
_POSITION_RE,
|
|
33
28
|
compute_cardinality,
|
|
34
29
|
write_csv,
|
|
35
30
|
write_csv_wide,
|
|
@@ -72,35 +67,8 @@ def _list_fmt_callback(ctx, param, value):
|
|
|
72
67
|
raise click.BadParameter(f"'{value}' is not one of 'csv', 'json', 'raw'")
|
|
73
68
|
|
|
74
69
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
"--host",
|
|
78
|
-
default=None,
|
|
79
|
-
envvar="SIGNALK_HOST",
|
|
80
|
-
help="SignalK server base URL. http:// added if scheme omitted. "
|
|
81
|
-
"Discovered via mDNS if omitted.",
|
|
82
|
-
)(f)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def _resolve_host(host: str | None, no_cache: bool = False) -> str:
|
|
86
|
-
"""Return a normalised host URL, discovering via mDNS if none provided."""
|
|
87
|
-
if host:
|
|
88
|
-
return normalise_host(host)
|
|
89
|
-
if not no_cache:
|
|
90
|
-
cached = get_cached_host()
|
|
91
|
-
if cached:
|
|
92
|
-
click.echo(f"Using cached host: {cached}", err=True)
|
|
93
|
-
return cached
|
|
94
|
-
click.echo("No host specified — searching for SignalK via mDNS...", err=True)
|
|
95
|
-
discovered = discover_host()
|
|
96
|
-
if not discovered:
|
|
97
|
-
raise click.UsageError(
|
|
98
|
-
"No SignalK server found via mDNS. Use --host or set SIGNALK_HOST."
|
|
99
|
-
)
|
|
100
|
-
click.echo(f"Discovered: {discovered}", err=True)
|
|
101
|
-
if not no_cache:
|
|
102
|
-
save_cached_host(discovered)
|
|
103
|
-
return discovered
|
|
70
|
+
_host_option = host_option
|
|
71
|
+
_resolve_host = resolve_host
|
|
104
72
|
|
|
105
73
|
|
|
106
74
|
def _provider_options(f):
|
|
@@ -127,18 +95,8 @@ def _time_options(f):
|
|
|
127
95
|
return f
|
|
128
96
|
|
|
129
97
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
"--bare",
|
|
133
|
-
is_flag=True,
|
|
134
|
-
help="Suppress all informational messages, outputting data only.",
|
|
135
|
-
)(f)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def _stderr_ctx(bare: bool) -> contextlib.AbstractContextManager:
|
|
139
|
-
return (
|
|
140
|
-
contextlib.redirect_stderr(io.StringIO()) if bare else contextlib.nullcontext()
|
|
141
|
-
)
|
|
98
|
+
_bare_option = bare_option
|
|
99
|
+
_stderr_ctx = stderr_ctx
|
|
142
100
|
|
|
143
101
|
|
|
144
102
|
def _build_time_params(from_: str | None, to: str | None, duration: str | None) -> dict:
|
|
@@ -341,7 +299,7 @@ def query(
|
|
|
341
299
|
# Generate auto-named file path now that format is known
|
|
342
300
|
if auto_name:
|
|
343
301
|
server_name = urlparse(host).hostname or re.sub(r"[^\w.-]", "_", host)
|
|
344
|
-
ts = datetime.now(
|
|
302
|
+
ts = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
|
|
345
303
|
ext = (
|
|
346
304
|
".feather"
|
|
347
305
|
if fmt == "feather"
|