diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 0e806d3..0bfe2b1 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -18,7 +18,7 @@ jobs: name: Set up Go uses: actions/setup-go@v5 with: - go-version: 1.18 + go-version: 1.23 - name: Verify dependencies diff --git a/.gitignore b/.gitignore index a0c4fb7..7b58d3b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ notecard-schema/ dist/ +bin/ -.DS_Store \ No newline at end of file +.DS_Store diff --git a/go.mod b/go.mod index f514ac1..52fd8f9 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/note-cli -go 1.15 +go 1.23.0 + +toolchain go1.23.3 replace github.com/blues/note-cli/lib => ./lib @@ -13,6 +15,30 @@ require ( github.com/fatih/color v1.17.0 github.com/peterh/liner v1.2.2 github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 + github.com/spf13/viper v1.21.0 +) + +require ( + github.com/blues/notehub-go v0.0.0-20260105133531-1e40c1ed371c // indirect + github.com/creack/goselect v0.1.2 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-viper/mapstructure/v2 v2.4.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect + github.com/spf13/afero v1.15.0 // indirect + github.com/spf13/cast v1.10.0 // indirect + github.com/spf13/pflag v1.0.10 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + github.com/tklauser/numcpus v0.8.0 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/text v0.28.0 // indirect + gopkg.in/validator.v2 v2.0.1 // indirect + periph.io/x/conn/v3 v3.7.0 // indirect ) require ( @@ -24,8 +50,9 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/shirou/gopsutil/v3 v3.24.4 // indirect github.com/shoenig/go-m1cpu v0.1.7 // indirect + github.com/spf13/cobra v1.10.1 github.com/tklauser/go-sysconf v0.3.14 // indirect go.bug.st/serial v1.6.2 - golang.org/x/sys v0.20.0 // indirect + golang.org/x/sys v0.34.0 // indirect periph.io/x/host/v3 v3.8.2 // indirect ) diff --git a/go.sum b/go.sum index 54c87ec..4e83bfa 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,11 @@ github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= -github.com/blues/note-go v1.5.0/go.mod h1:F66ZqObdOhxRRXIwn9+YhVGqB93jMAnqlO2ibwMa998= github.com/blues/note-go v1.7.4 h1:AqeU6HXkCa7FwDsAao49H6DdTTtNNGJYjGwevZi4Shc= github.com/blues/note-go v1.7.4/go.mod h1:GfslvbmFus7z05P1YykcbMedTKTuDNTf8ryBb1Qjq/4= +github.com/blues/notehub-go v0.0.0-20251217210155-025b72380e33 h1:p06KaLsAkiNPG6Ae8Hlng9QgMBmo1VBhXgkUMyhG+dg= +github.com/blues/notehub-go v0.0.0-20251217210155-025b72380e33/go.mod h1:a8fgPV3iznI6oIPTTzAGrXnq4vBD3d3jvKEnjAWtC/o= +github.com/blues/notehub-go v0.0.0-20260105133531-1e40c1ed371c h1:C0cH1ggVjJw4chKUF8ZG0Fd24u1F8OZYlYfJJgXwdGI= +github.com/blues/notehub-go v0.0.0-20260105133531-1e40c1ed371c/go.mod h1:a8fgPV3iznI6oIPTTzAGrXnq4vBD3d3jvKEnjAWtC/o= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -9,10 +13,16 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= +github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= +github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/gofrs/flock v0.7.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= @@ -20,10 +30,14 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jonboulle/clockwork v0.3.0 h1:9BSCMi8C+0qdApAp4auwX0RkLGUjs956h0EkuQymUhg= github.com/jonboulle/clockwork v0.3.0/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae h1:dIZY4ULFcto4tAFlj1FYZl8ztUZ13bdq+PLY+NOfbyI= github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k= @@ -35,8 +49,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/peterh/liner v1.2.2 h1:aJ4AOodmL+JxOZZEL2u9iJf8omNRpqHc/EbrK+3mAXw= github.com/peterh/liner v1.2.2/go.mod h1:xFwJyiKIXJZUKItq5dGHZSTBRAuG/CpeNpWLyiNRNwI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -47,6 +61,11 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:Om github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= +github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 h1:lZUw3E0/J3roVtGQ+SCrUrg3ON6NgVqpn3+iol9aGu4= github.com/santhosh-tekuri/jsonschema/v5 v5.3.1/go.mod h1:uToXkOrWAZ6/Oc07xWQrPOhJotwFIyu2bBVN41fcDUY= github.com/shirou/gopsutil/v3 v3.21.6/go.mod h1:JfVbDpIBLVzT8oKbvMg9P3wEIMDDpVn+LwHTKj0ST88= @@ -58,18 +77,33 @@ github.com/shoenig/go-m1cpu v0.1.7/go.mod h1:KkDOw6m3ZJQAPHbrzkZki4hnx+pDRR1Lo+l github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk= github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= +github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= +github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= +github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= +github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= +github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s= +github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= +github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= +github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tklauser/go-sysconf v0.3.6/go.mod h1:MkWzOF4RMCshBAMXuhXJs64Rte09mITnppBXY/rYEFI= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU= @@ -80,15 +114,14 @@ github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYg github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= -go.bug.st/serial v1.3.4/go.mod h1:z8CesKorE90Qr/oRSJiEuvzYRKol9r/anJZEb5kt304= go.bug.st/serial v1.6.1/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE= go.bug.st/serial v1.6.2 h1:kn9LRX3sdm+WxWKufMlIRndwGfPWsH1/9lCWXQCasq8= go.bug.st/serial v1.6.2/go.mod h1:UABfsluHAiaNI+La2iESysd9Vetq7VRdpxvjx7CmmOE= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210316164454-77fc1eacc6aa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211117180635-dee7805ff2e1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -96,14 +129,17 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= +golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= +golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/validator.v2 v2.0.1 h1:xF0KWyGWXm/LM2G1TrEjqOu4pa6coO9AlWSf3msVfDY= +gopkg.in/validator.v2 v2.0.1/go.mod h1:lIUZBlB3Im4s/eYp39Ry/wkR02yOPhZ9IwIRBjuPuG8= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= @@ -113,4 +149,3 @@ periph.io/x/d2xx v0.1.0/go.mod h1:OflHQcWZ4LDP/2opGYbdXSP/yvWSnHVFO90KRoyobWY= periph.io/x/host/v3 v3.8.0/go.mod h1:rzOLH+2g9bhc6pWZrkCrmytD4igwQ2vxFw6Wn6ZOlLY= periph.io/x/host/v3 v3.8.2 h1:ayKUDzgUCN0g8+/xM9GTkWaOBhSLVcVHGTfjAOi8OsQ= periph.io/x/host/v3 v3.8.2/go.mod h1:yFL76AesNHR68PboofSWYaQTKmvPXsQH2Apvp/ls/K4= -periph.io/x/periph v3.6.2+incompatible/go.mod h1:EWr+FCIU2dBWz5/wSWeiIUJTriYv9v2j2ENBmgYyy7Y= diff --git a/notehub/app.go b/notehub/app.go deleted file mode 100644 index 8bdd76d..0000000 --- a/notehub/app.go +++ /dev/null @@ -1,337 +0,0 @@ -// Copyright 2024 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. - -package main - -import ( - "bufio" - "bytes" - "fmt" - "os" - "sort" - "strings" - - "github.com/blues/note-cli/lib" - notegoapi "github.com/blues/note-go/notehub/api" -) - -type Metadata struct { - Name string `json:"name,omitempty"` - UID string `json:"uid,omitempty"` - BA string `json:"billing_account_uid,omitempty"` - Vars map[string]string `json:"vars,omitempty"` -} - -type AppMetadata struct { - App Metadata `json:"app,omitempty"` - Fleets []Metadata `json:"fleets,omitempty"` - Routes []Metadata `json:"routes,omitempty"` - Products []Metadata `json:"products,omitempty"` -} - -// Load metadata for the app -func appGetMetadata(flagVerbose bool, flagVars bool) (appMetadata AppMetadata, err error) { - - rsp := map[string]interface{}{} - err = reqHubV0(flagVerbose, lib.ConfigAPIHub(), []byte("{\"req\":\"hub.app.get\"}"), "", "", "", "", false, false, nil, &rsp) - if err != nil { - return - } - rsperr, _ := rsp["err"].(string) - if rsperr != "" { - err = fmt.Errorf("%s", rsperr) - return - } - - // App info - appMetadata.App.UID, _ = rsp["uid"].(string) - appMetadata.App.Name, _ = rsp["label"].(string) - appMetadata.App.BA, _ = rsp["billing_account_uid"].(string) - - // Fleet info - settings, exists := rsp["info"].(map[string]interface{}) - if exists { - fleets, exists := settings["fleet"].(map[string]interface{}) - if exists { - items := []Metadata{} - for k, v := range fleets { - vj, ok := v.(map[string]interface{}) - if ok { - i := Metadata{Name: vj["label"].(string), UID: k} - if flagVars { - varsRsp := notegoapi.GetFleetEnvironmentVariablesResponse{} - url := fmt.Sprintf("/v1/projects/%s/fleets/%s/environment_variables", appMetadata.App.UID, k) - err = reqHubV1(flagVerbose, lib.ConfigAPIHub(), "GET", url, nil, &varsRsp) - if err != nil { - return - } - i.Vars = varsRsp.EnvironmentVariables - } - items = append(items, i) - } - } - appMetadata.Fleets = items - } - } - - // Enum routes - rsp = map[string]interface{}{} - err = reqHubV0(flagVerbose, lib.ConfigAPIHub(), []byte("{\"req\":\"hub.app.test.route\"}"), "", "", "", "", false, false, nil, &rsp) - rsperr, _ = rsp["err"].(string) - if rsperr != "" { - err = fmt.Errorf("%s", rsperr) - } - if err == nil { - body, exists := rsp["body"].(map[string]interface{}) - if exists { - items := []Metadata{} - for k, v := range body { - vs, ok := v.(string) - if ok { - components := strings.Split(k, "/") - if len(components) > 1 { - i := Metadata{Name: vs, UID: components[1]} - items = append(items, i) - } - } - } - appMetadata.Routes = items - } - } - - // Products - rsp = map[string]interface{}{} - err = reqHubV1(flagVerbose, lib.ConfigAPIHub(), "GET", "/v1/projects/"+appMetadata.App.UID+"/products", nil, &rsp) - if err == nil { - pi, exists := rsp["products"].([]interface{}) - if exists { - items := []Metadata{} - for _, v := range pi { - p, ok := v.(map[string]interface{}) - if ok { - i := Metadata{Name: p["label"].(string), UID: p["uid"].(string)} - items = append(items, i) - } - appMetadata.Products = items - } - } - } - - // Done - return - -} - -// Get a device list given -func appGetScope(scope string, flagVerbose bool) (appMetadata AppMetadata, scopeDevices []string, scopeFleets []string, err error) { - - // Process special scopes, which are handled inside addScope - switch scope { - case "devices": - scope = "@" - case "fleets": - scope = "-" - } - - // Get the metadata before we begin, because at a minimum we need appUID - appMetadata, err = appGetMetadata(flagVerbose, false) - if err != nil { - return - } - - // On the command line (but not inside files) we allow comma-separated lists - if strings.Contains(scope, ",") { - scopeList := strings.Split(scope, ",") - for _, scope := range scopeList { - err = addScope(scope, &appMetadata, &scopeDevices, &scopeFleets, flagVerbose) - if err != nil { - return - } - } - } else { - err = addScope(scope, &appMetadata, &scopeDevices, &scopeFleets, flagVerbose) - if err != nil { - return - } - } - - // Remove duplicates - scopeDevices = sortAndRemoveDuplicates(scopeDevices) - scopeFleets = sortAndRemoveDuplicates(scopeFleets) - - // Done - return - -} - -// Recursively add scope -func addScope(scope string, appMetadata *AppMetadata, scopeDevices *[]string, scopeFleets *[]string, flagVerbose bool) (err error) { - - if strings.HasPrefix(scope, "dev:") { - *scopeDevices = append(*scopeDevices, scope) - return - } - - if strings.HasPrefix(scope, "imei:") || strings.HasPrefix(scope, "burn:") { - // This is a pre-V1 legacy that still exists in some ancient fleets - *scopeDevices = append(*scopeDevices, scope) - return - } - - if strings.HasPrefix(scope, "fleet:") { - *scopeFleets = append(*scopeFleets, scope) - return - } - - // See if this is a fleet name, and translate it to an ID - if !strings.HasPrefix(scope, "@") { - found := false - for _, fleet := range (*appMetadata).Fleets { - if fleetMatchesScope(fleet.Name, scope) { - *scopeFleets = append(*scopeFleets, fleet.UID) - found = true - } - } - if !found { - return fmt.Errorf("'%s' does not appear to be a device, fleet, @fleet indirection, or @file.ext indirection", scope) - } - return - } - - // Process a fleet indirection. First, find the fleet. - indirectScope := strings.TrimPrefix(scope, "@") - foundFleet := false - lookingFor := strings.TrimSpace(indirectScope) - - // Looking for "all devices" or a named fleet - if indirectScope == "" { - // All devices - - pageSize := 500 - pageNum := 0 - for { - pageNum++ - - devices := notegoapi.GetDevicesResponse{} - url := fmt.Sprintf("/v1/projects/%s/devices?pageSize=%d&pageNum=%d", appMetadata.App.UID, pageSize, pageNum) - err = reqHubV1(flagVerbose, lib.ConfigAPIHub(), "GET", url, nil, &devices) - if err != nil { - return - } - - for _, device := range devices.Devices { - err = addScope(device.UID, appMetadata, scopeDevices, scopeFleets, flagVerbose) - if err != nil { - return err - } - } - - if !devices.HasMore { - break - } - - } - - return - - } else { - - // Fleet - for _, fleet := range (*appMetadata).Fleets { - if lookingFor == fleet.UID || fleetMatchesScope(fleet.Name, lookingFor) { - foundFleet = true - - pageSize := 100 - pageNum := 0 - for { - pageNum++ - - devices := notegoapi.GetDevicesResponse{} - url := fmt.Sprintf("/v1/projects/%s/fleets/%s/devices?pageSize=%d&pageNum=%d", appMetadata.App.UID, fleet.UID, pageSize, pageNum) - err = reqHubV1(flagVerbose, lib.ConfigAPIHub(), "GET", url, nil, &devices) - if err != nil { - return - } - - for _, device := range devices.Devices { - err = addScope(device.UID, appMetadata, scopeDevices, scopeFleets, flagVerbose) - if err != nil { - return err - } - } - - if !devices.HasMore { - break - } - - } - - } - } - if foundFleet { - return - } - - } - - // Process a file indirection - var contents []byte - contents, err = os.ReadFile(indirectScope) - if err != nil { - return fmt.Errorf("%s: %s", indirectScope, err) - } - - scanner := bufio.NewScanner(bytes.NewReader(contents)) - scanner.Split(bufio.ScanLines) - - for scanner.Scan() { - line := scanner.Text() - if trimmedLine := strings.TrimSpace(line); trimmedLine != "" { - err = addScope(trimmedLine, appMetadata, scopeDevices, scopeFleets, flagVerbose) - if err != nil { - return err - } - } - } - - err = scanner.Err() - return - -} - -// Sort and remove duplicates in a string slice -func sortAndRemoveDuplicates(strings []string) []string { - - sort.Strings(strings) - - unique := make(map[string]struct{}) - var result []string - - for _, v := range strings { - if _, exists := unique[v]; !exists { - unique[v] = struct{}{} - result = append(result, v) - } - } - - return result -} - -// See if a fleet name matches a scope name -func fleetMatchesScope(fleetName string, scope string) bool { - normalizedScope := strings.ToLower(scope) - scopeWildcard := false - if strings.HasSuffix(normalizedScope, "*") { - normalizedScope = strings.TrimSuffix(normalizedScope, "*") - scopeWildcard = true - } - normalizedName := strings.ToLower(fleetName) - match := scope == "-" || normalizedName == normalizedScope - if scopeWildcard { - if strings.HasPrefix(normalizedName, normalizedScope) { - match = true - } - } - return match -} diff --git a/notehub/auth.go b/notehub/auth.go deleted file mode 100644 index 443e9f0..0000000 --- a/notehub/auth.go +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright 2017 Blues Inc. All rights reserved. -// Use of this source code is governed by licenses granted by the -// copyright holder including that found in the LICENSE file. - -package main - -import ( - "fmt" - - "github.com/blues/note-cli/lib" - "github.com/blues/note-go/notehub" -) - -// Sign into the notehub account with a personal access token -func authSignInToken(personalAccessToken string) error { - // TODO: maybe call configInit() to set defaults? - config, err := lib.GetConfig() - if err != nil { - return err - } - - // Print hub if not the default - fmt.Printf("notehub: %s\n", config.Hub) - - email, err := lib.IntrospectToken(config.Hub, personalAccessToken) - if err != nil { - return err - } - - config.SetDefaultCredentials(personalAccessToken, email, nil) - - if err := config.Write(); err != nil { - return err - } - - // Done - fmt.Printf("signed in successfully with token\n") - return nil -} - -// Sign into the Notehub account with browser-based OAuth2 flow -func authSignIn() error { - - // load config - config, err := lib.GetConfig() - if err != nil { - return err - } - - credentials := config.DefaultCredentials() - - // if signed in with an access token via OAuth, then revoke the access token - // we don't want to revoke a PAT because the user explicitly set an - // expiration date on that token - if credentials != nil && credentials.IsOAuthAccessToken() { - if err := config.RemoveDefaultCredentials(); err != nil { - return err - } - } - - // initiate the browser-based OAuth2 login flow - accessToken, err := notehub.InitiateBrowserBasedLogin(config.Hub) - if err != nil { - return fmt.Errorf("authentication failed: %w", err) - } - - config.SetDefaultCredentials(accessToken.AccessToken, accessToken.Email, &accessToken.ExpiresAt) - - // save the config with the new credentials - if err := config.Write(); err != nil { - return err - } - - // print out information about the session - if accessToken != nil { - fmt.Printf("%s\n", banner()) - fmt.Printf("signed in as %s\n", accessToken.Email) - fmt.Printf("token expires at %s\n", accessToken.ExpiresAt.Format("2006-01-02 15:04:05 MST")) - } - - // Done - return nil -} - -// Banner for authentication -// http://patorjk.com/software/taag -// "Big" font - -func banner() (s string) { - s += " _ _ _ \r\n" - s += " | | | | | | \r\n" - s += " _ __ ___ | |_ ___| |__ _ _| |__ \r\n" - s += "| '_ \\ / _ \\| __/ _ \\ '_ \\| | | | '_ \\ \r\n" - s += "| | | | (_) | || __/ | | | |_| | |_) | \r\n" - s += "|_| |_|\\___/ \\__\\___|_| |_|\\__,_|_.__/ \r\n" - s += "\r\n" - return -} diff --git a/notehub/build b/notehub/build deleted file mode 100644 index 6d49256..0000000 --- a/notehub/build +++ /dev/null @@ -1 +0,0 @@ -Sun Mar 6 10:36:20 EST 2022 diff --git a/notehub/cmd/auth.go b/notehub/cmd/auth.go new file mode 100644 index 0000000..568d2be --- /dev/null +++ b/notehub/cmd/auth.go @@ -0,0 +1,319 @@ +// Copyright 2025 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package cmd + +import ( + "bufio" + "fmt" + "os" + "strconv" + "strings" + + "github.com/blues/note-go/notehub" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// Auth command flags +var ( + flagSetProject string +) + +// authCmd represents the auth command +var authCmd = &cobra.Command{ + Use: "auth", + Short: "Authentication commands", + Long: `Commands for signing in, signing out, and managing authentication tokens.`, +} + +// signinCmd represents the signin command +var signinCmd = &cobra.Command{ + Use: "signin", + Short: "Sign in to Notehub", + Long: `Sign in to Notehub using browser-based OAuth2 flow.`, + RunE: func(cmd *cobra.Command, args []string) error { + credentials, err := GetHubCredentials() + if err != nil { + return err + } + + // if signed in with an access token via OAuth, then revoke the access token + // we don't want to revoke a PAT because the user explicitly set an + // expiration date on that token + if credentials != nil && credentials.IsOAuthAccessToken() { + if err := RemoveHubCredentials(); err != nil { + return err + } + } + + // initiate the browser-based OAuth2 login flow + hub := GetHub() + accessToken, err := notehub.InitiateBrowserBasedLogin(hub) + if err != nil { + return fmt.Errorf("authentication failed: %w", err) + } + + // save the credentials + if err := SetHubCredentials(accessToken.AccessToken, accessToken.Email, &accessToken.ExpiresAt); err != nil { + return err + } + + // print out information about the session + if accessToken != nil { + fmt.Printf("%s\n", banner()) + fmt.Printf("signed in as %s\n", accessToken.Email) + fmt.Printf("token expires at %s\n", accessToken.ExpiresAt.Format("2006-01-02 15:04:05 MST")) + } + + // Set project if provided via flag or prompt for selection + if err := handleProjectSelection(flagSetProject); err != nil { + return err + } + + return nil + }, +} + +// signinTokenCmd represents the signin-token command +var signinTokenCmd = &cobra.Command{ + Use: "signin-token [token]", + Short: "Sign in with a personal access token", + Long: `Sign in to Notehub using a personal access token.`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + personalAccessToken := args[0] + + hub := GetHub() + // Print hub if not the default + fmt.Printf("notehub: %s\n", hub) + + email, err := IntrospectToken(hub, personalAccessToken) + if err != nil { + return err + } + + if err := SetHubCredentials(personalAccessToken, email, nil); err != nil { + return err + } + + // Done + fmt.Printf("signed in successfully with token\n") + + // Set project if provided via flag or prompt for selection + if err := handleProjectSelection(flagSetProject); err != nil { + return err + } + + return nil + }, +} + +// signoutCmd represents the signout command +var signoutCmd = &cobra.Command{ + Use: "signout", + Short: "Sign out of Notehub", + Long: `Sign out of Notehub and remove stored credentials.`, + RunE: func(cmd *cobra.Command, args []string) error { + if err := RemoveHubCredentials(); err != nil { + return err + } + + // Also clear project setting + viper.Set("project", "") + if err := SaveConfig(); err != nil { + return fmt.Errorf("failed to save config: %w", err) + } + + fmt.Printf("signed out successfully\n") + return nil + }, +} + +// tokenCmd represents the token command +var tokenCmd = &cobra.Command{ + Use: "token", + Short: "Display the current authentication token", + Long: `Display the current authentication token for the signed-in account.`, + RunE: func(cmd *cobra.Command, args []string) error { + credentials, err := GetHubCredentials() + if err != nil { + return err + } + + if credentials == nil { + return fmt.Errorf("please sign in using 'notehub auth signin' or 'notehub auth signin-token'") + } + + fmt.Printf("%s\n", credentials.Token) + return nil + }, +} + +func init() { + rootCmd.AddCommand(authCmd) + authCmd.AddCommand(signinCmd) + authCmd.AddCommand(signinTokenCmd) + authCmd.AddCommand(signoutCmd) + authCmd.AddCommand(tokenCmd) + + // Add --set-project flag to signin commands + signinCmd.Flags().StringVar(&flagSetProject, "set-project", "", "Automatically set project after signin (name or UID)") + signinTokenCmd.Flags().StringVar(&flagSetProject, "set-project", "", "Automatically set project after signin (name or UID)") +} + +// Banner for authentication +// http://patorjk.com/software/taag +// "Big" font +func banner() (s string) { + s += " _ _ _ \r\n" + s += " | | | | | | \r\n" + s += " _ __ ___ | |_ ___| |__ _ _| |__ \r\n" + s += "| '_ \\ / _ \\| __/ _ \\ '_ \\| | | | '_ \\ \r\n" + s += "| | | | (_) | || __/ | | | |_| | |_) | \r\n" + s += "|_| |_|\\___/ \\__\\___|_| |_|\\__,_|_.__/ \r\n" + s += "\r\n" + return +} + +// handleProjectSelection handles project selection after signin via flag or interactive prompt +func handleProjectSelection(projectFlag string) error { + // Check if a project is already set + currentProject := GetProject() + if currentProject != "" { + // Project already configured, no need to prompt + return nil + } + + // If project flag was provided, set it directly + if projectFlag != "" { + return setProjectByIdentifier(projectFlag) + } + + // Otherwise, offer interactive selection + return interactiveProjectSelection() +} + +// setProjectByIdentifier sets a project by name or UID (from project.go logic) +func setProjectByIdentifier(identifier string) error { + // Get SDK client + client := GetNotehubClient() + ctx, err := GetNotehubContext() + if err != nil { + return err + } + + // First, try to use it directly as a UID + project, resp, err := client.ProjectAPI.GetProject(ctx, identifier).Execute() + + // If that failed, it might be a project name - fetch all projects and search + if err != nil || (resp != nil && resp.StatusCode == 404) { + projectsRsp, _, err := client.ProjectAPI.GetProjects(ctx).Execute() + if err != nil { + return fmt.Errorf("failed to list projects: %w", err) + } + + // Search for project by name (exact match) + found := false + for _, proj := range projectsRsp.Projects { + if proj.Label == identifier { + project = &proj + found = true + break + } + } + + if !found { + return fmt.Errorf("project '%s' not found", identifier) + } + } + + // Save to config + viper.Set("project", project.Uid) + if err := SaveConfig(); err != nil { + return fmt.Errorf("failed to save config: %w", err) + } + + fmt.Printf("\nActive project set to: %s\n", project.Label) + fmt.Printf("Project UID: %s\n\n", project.Uid) + + return nil +} + +// interactiveProjectSelection prompts the user to select a project interactively +func interactiveProjectSelection() error { + // Get SDK client + client := GetNotehubClient() + ctx, err := GetNotehubContext() + if err != nil { + return err + } + + // Fetch all projects + projectsRsp, _, err := client.ProjectAPI.GetProjects(ctx).Execute() + if err != nil { + // If we can't fetch projects, just show instructions + fmt.Println() + fmt.Println("To get started, you'll need to select a project to work with.") + fmt.Println("Run 'notehub project list' to see your available projects,") + fmt.Println("then 'notehub project set ' to select one.") + fmt.Println() + return nil + } + + if len(projectsRsp.Projects) == 0 { + fmt.Println() + fmt.Println("No projects found. You can create a new project at https://notehub.io") + fmt.Println() + return nil + } + + // Display projects with numbers + fmt.Println() + fmt.Println("Select a project to work with:") + fmt.Println() + for i, project := range projectsRsp.Projects { + fmt.Printf(" %d) %s\n", i+1, project.Label) + } + fmt.Println() + fmt.Printf("Enter project number (1-%d), or press Enter to skip: ", len(projectsRsp.Projects)) + + // Read user input + reader := bufio.NewReader(os.Stdin) + input, err := reader.ReadString('\n') + if err != nil { + return nil // Skip on error + } + + input = strings.TrimSpace(input) + if input == "" { + // User pressed Enter, skip selection + fmt.Println() + fmt.Println("Skipped project selection. You can set a project later with 'notehub project set '") + fmt.Println() + return nil + } + + // Parse selection + selection, err := strconv.Atoi(input) + if err != nil || selection < 1 || selection > len(projectsRsp.Projects) { + fmt.Println() + fmt.Printf("Invalid selection. You can set a project later with 'notehub project set '\n") + fmt.Println() + return nil + } + + // Set the selected project + selectedProject := projectsRsp.Projects[selection-1] + viper.Set("project", selectedProject.Uid) + if err := SaveConfig(); err != nil { + return fmt.Errorf("failed to save config: %w", err) + } + + fmt.Println() + fmt.Printf("Active project set to: %s\n", selectedProject.Label) + fmt.Printf("Project UID: %s\n\n", selectedProject.Uid) + + return nil +} diff --git a/notehub/cmd/config.go b/notehub/cmd/config.go new file mode 100644 index 0000000..ef683ac --- /dev/null +++ b/notehub/cmd/config.go @@ -0,0 +1,324 @@ +// Copyright 2025 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package cmd + +import ( + "errors" + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "strings" + "time" + + "github.com/blues/note-go/note" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// Credentials represent Notehub authentication credentials +type Credentials struct { + User string `json:"user,omitempty" mapstructure:"user"` + Token string `json:"token,omitempty" mapstructure:"token"` + ExpiresAt *time.Time `json:"expires_at,omitempty" mapstructure:"expires_at"` + Hub string `json:"-" mapstructure:"-"` +} + +// IsOAuthAccessToken checks if the token is an OAuth access token (vs PAT) +func (creds Credentials) IsOAuthAccessToken() bool { + personalAccessTokenPrefixes := []string{"ory_st_", "api_key_"} + for _, prefix := range personalAccessTokenPrefixes { + if strings.HasPrefix(creds.Token, prefix) { + return false + } + } + return true +} + +// AddHttpAuthHeader adds the authorization header to an HTTP request +func (creds Credentials) AddHttpAuthHeader(req *http.Request) { + req.Header.Set("Authorization", "Bearer "+creds.Token) +} + +// IntrospectToken validates a token and returns the associated email +func IntrospectToken(hub string, token string) (string, error) { + if !strings.HasPrefix(hub, "api.") { + hub = "api." + hub + } + + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://%s/userinfo", hub), nil) + if err != nil { + return "", err + } + + req.Header.Set("Authorization", "Bearer "+token) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", err + } + + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + defer resp.Body.Close() + + userinfo := map[string]interface{}{} + if err := note.JSONUnmarshal(body, &userinfo); err != nil { + return "", err + } + + if resp.StatusCode != http.StatusOK { + err := userinfo["err"] + return "", fmt.Errorf("%s (http %d)", err, resp.StatusCode) + } + + if email, ok := userinfo["email"].(string); !ok || email == "" { + fmt.Printf("response: %s\n", userinfo) + return "", fmt.Errorf("error introspecting token: no email in response") + } else { + return email, nil + } +} + +// Validate checks if credentials are valid +func (creds *Credentials) Validate() error { + if creds == nil { + return errors.New("no credentials specified") + } + _, err := IntrospectToken(creds.Hub, creds.Token) + return err +} + +// GetHub returns the currently configured Notehub hub +func GetHub() string { + hub := viper.GetString("hub") + if hub == "" { + hub = "notehub.io" // default + } + return hub +} + +// SetHub sets the Notehub hub +func SetHub(hub string) { + viper.Set("hub", hub) +} + +// GetCredentials returns credentials for the current hub +func GetHubCredentials() (*Credentials, error) { + hub := GetHub() + + // Viper treats dots in keys as nested paths, so "notehub.io" becomes "notehub.io" + // We need to access it using the dot notation that Viper creates + credsMap := viper.GetStringMap(fmt.Sprintf("credentials.%s", hub)) + if len(credsMap) == 0 { + return nil, nil + } + + creds := &Credentials{ + Hub: hub, + } + + if user, ok := credsMap["user"].(string); ok { + creds.User = user + } + if token, ok := credsMap["token"].(string); ok { + creds.Token = token + } + if expiresAt, ok := credsMap["expires_at"].(string); ok { + if t, err := time.Parse(time.RFC3339, expiresAt); err == nil { + creds.ExpiresAt = &t + } + } + + if creds.User == "" || creds.Token == "" { + return nil, nil + } + + return creds, nil +} + +// SetCredentials sets credentials for the current hub +func SetHubCredentials(token, user string, expiresAt *time.Time) error { + hub := GetHub() + + // Viper treats dots as path separators, so we use dot notation to set nested values + // For "notehub.io", this creates credentials.notehub.io structure + viper.Set(fmt.Sprintf("credentials.%s.user", hub), user) + viper.Set(fmt.Sprintf("credentials.%s.token", hub), token) + if expiresAt != nil { + viper.Set(fmt.Sprintf("credentials.%s.expires_at", hub), expiresAt.Format(time.RFC3339)) + } else { + viper.Set(fmt.Sprintf("credentials.%s.expires_at", hub), nil) + } + + return SaveConfig() +} + +// RemoveCredentials removes credentials for the current hub +func RemoveHubCredentials() error { + hub := GetHub() + + credentials, err := GetHubCredentials() + if err != nil { + return err + } + if credentials == nil { + return fmt.Errorf("not signed in to %s", hub) + } + + // If OAuth access token, revoke it + if credentials.IsOAuthAccessToken() { + // Revoke token logic would go here if needed + // For now, we just remove it from config + } + + // Remove credentials by clearing each field explicitly + viper.Set(fmt.Sprintf("credentials.%s.user", hub), "") + viper.Set(fmt.Sprintf("credentials.%s.token", hub), "") + viper.Set(fmt.Sprintf("credentials.%s.expires_at", hub), "") + + return SaveConfig() +} + +// SaveConfig writes the current viper configuration to disk +func SaveConfig() error { + configPath := getConfigPath() + + // Ensure the config directory exists + configDir := filepath.Dir(configPath) + if err := os.MkdirAll(configDir, 0755); err != nil { + return fmt.Errorf("failed to create config directory: %w", err) + } + + // Write the config file + if err := viper.WriteConfigAs(configPath); err != nil { + return fmt.Errorf("failed to write config: %w", err) + } + + return nil +} + +// getConfigPath returns the path to the config file +func getConfigPath() string { + // Use the same config directory as lib/config.go + home, err := os.UserHomeDir() + if err != nil { + return filepath.Join(".", ".notehub", "config.yaml") + } + return filepath.Join(home, ".notehub", "config.yaml") +} + +// GetAPIHub returns the API hub URL +func GetAPIHub() string { + hub := GetHub() + if !strings.HasPrefix(hub, "api.") { + hub = "api." + hub + } + return hub +} + +// AddAuthenticationHeader adds authentication header to an HTTP request +func AddAuthenticationHeader(httpReq *http.Request) error { + credentials, err := GetHubCredentials() + if err != nil { + return err + } + + if credentials == nil { + hub := GetHub() + return fmt.Errorf("not authenticated to %s: please use 'notehub auth signin' to sign into the Notehub service", hub) + } + + // Set the header + httpReq.Header.Set("Authorization", "Bearer "+credentials.Token) + + return nil +} + +// configCmd represents the config command +var configCmd = &cobra.Command{ + Use: "config", + Short: "Display current configuration", + Long: `Display the current configuration including hub, credentials, and flag values.`, + Run: func(cmd *cobra.Command, args []string) { + displayConfig() + }, +} + +func init() { + rootCmd.AddCommand(configCmd) +} + +// displayConfig prints the current configuration in a readable format +func displayConfig() { + fmt.Println("\nCurrent Configuration:") + fmt.Println("=====================") + + // Display hub + hub := GetHub() + fmt.Printf("\nHub: %s\n", hub) + + // Display credentials + credentials, _ := GetHubCredentials() + if credentials != nil && credentials.User != "" { + fmt.Println("\nCredentials:") + fmt.Printf(" %s:\n", hub) + fmt.Printf(" User: %s\n", credentials.User) + + // Determine token type + tokenType := "OAuth" + if !credentials.IsOAuthAccessToken() { + tokenType = "Personal Access Token" + } + + // Check expiration + expires := "" + if credentials.ExpiresAt != nil { + if credentials.ExpiresAt.Before(time.Now()) { + expires = " [EXPIRED]" + } else { + expires = fmt.Sprintf(" (expires: %s)", credentials.ExpiresAt.Format("2006-01-02 15:04")) + } + } + + fmt.Printf(" Type: %s%s\n", tokenType, expires) + } else { + fmt.Println("\nCredentials: None (not signed in)") + } + + // Display active flag values (only non-empty ones) + fmt.Println("\nActive Settings:") + + settings := []struct { + name string + value string + }{ + {"project", viper.GetString("project")}, + {"product", viper.GetString("product")}, + {"device", viper.GetString("device")}, + } + + hasSettings := false + for _, setting := range settings { + if setting.value != "" { + fmt.Printf(" %s: %s\n", setting.name, setting.value) + hasSettings = true + } + } + + if !hasSettings { + fmt.Println(" (none)") + } + + // Display config file location + configFile := viper.ConfigFileUsed() + if configFile == "" { + configFile = getConfigPath() + } + fmt.Printf("\nConfig file: %s\n\n", configFile) +} diff --git a/notehub/cmd/device.go b/notehub/cmd/device.go new file mode 100644 index 0000000..03d8b0a --- /dev/null +++ b/notehub/cmd/device.go @@ -0,0 +1,603 @@ +// Copyright 2025 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package cmd + +import ( + "fmt" + "strings" + "time" + + "github.com/blues/note-go/note" + notehub "github.com/blues/notehub-go" + "github.com/spf13/cobra" +) + +// deviceCmd represents the device command +var deviceCmd = &cobra.Command{ + Use: "device", + Short: "Manage Notehub devices", + Long: `Commands for listing and managing devices in Notehub projects.`, +} + +// deviceListCmd represents the device list command +var deviceListCmd = &cobra.Command{ + Use: "list", + Short: "List all devices", + Long: `List all devices in the current project or a specified project.`, + RunE: func(cmd *cobra.Command, args []string) error { + GetCredentials() // Validates and exits if not authenticated + + // Get project UID (from config or --project flag) + projectUID := GetProject() + if projectUID == "" { + return fmt.Errorf("no project set. Use 'notehub project set ' or provide --project flag") + } + + // Get devices using SDK + client := GetNotehubClient() + ctx, err := GetNotehubContext() + if err != nil { + return err + } + + devicesResp, _, err := client.DeviceAPI.GetDevices(ctx, projectUID).Execute() + if err != nil { + return fmt.Errorf("failed to list devices: %w", err) + } + + // Handle JSON output + if GetJson() || GetPretty() { + var output []byte + var err error + if GetPretty() { + output, err = note.JSONMarshalIndent(devicesResp, "", " ") + } else { + output, err = note.JSONMarshal(devicesResp) + } + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Printf("%s\n", output) + return nil + } + + if len(devicesResp.Devices) == 0 { + fmt.Println("No devices found in this project.") + return nil + } + + // Display devices in human-readable format + fmt.Printf("\nDevices in Project:\n") + fmt.Printf("===================\n\n") + + for _, device := range devicesResp.Devices { + fmt.Printf("Device: %s\n", device.Uid) + if device.SerialNumber != nil && *device.SerialNumber != "" { + fmt.Printf(" Serial Number: %s\n", *device.SerialNumber) + } + if device.ProductUid != "" { + fmt.Printf(" Product: %s\n", device.ProductUid) + } + if device.Sku != nil && *device.Sku != "" { + fmt.Printf(" Type: %s\n", *device.Sku) + } + if device.FirmwareNotecard != nil && *device.FirmwareNotecard != "" { + fmt.Printf(" Notecard Firmware: %s\n", *device.FirmwareNotecard) + } + if device.FirmwareHost != nil && *device.FirmwareHost != "" { + fmt.Printf(" Host Firmware: %s\n", *device.FirmwareHost) + } + if device.LastActivity.IsSet() { + if lastActivity := device.LastActivity.Get(); lastActivity != nil && !lastActivity.IsZero() { + fmt.Printf(" Last Activity: %s\n", lastActivity.Format("2006-01-02 15:04:05 MST")) + } + } + if !device.Provisioned.IsZero() { + fmt.Printf(" Provisioned: %s\n", device.Provisioned.Format("2006-01-02 15:04:05 MST")) + } + if device.FleetUids != nil && len(device.FleetUids) > 0 { + fmt.Printf(" Fleets: %d\n", len(device.FleetUids)) + } + fmt.Println() + } + + fmt.Printf("Total devices: %d\n", len(devicesResp.Devices)) + if devicesResp.HasMore { + fmt.Println("(showing first page of results)") + } + fmt.Println() + + return nil + }, +} + +// deviceEnableCmd represents the device enable command +var deviceEnableCmd = &cobra.Command{ + Use: "enable [scope]", + Short: "Enable one or more devices", + Long: `Enable one or more devices in a Notehub project, allowing them to communicate with Notehub. + +Scope Formats: + dev:xxxx Single device UID + imei:xxxx Device by IMEI + fleet:xxxx All devices in fleet (by UID) + production All devices in named fleet + @fleet-name All devices in fleet (indirection) + @ All devices in project + @devices.txt Device UIDs from file (one per line) + dev:aaa,dev:bbb Multiple scopes (comma-separated) + +Examples: + # Enable a single device + notehub device enable dev:864475046552567 + + # Enable all devices in a fleet + notehub device enable @production + + # Enable all devices in project + notehub device enable @ + + # Enable devices from a file + notehub device enable @devices.txt`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + GetCredentials() // Validates and exits if not authenticated + + scope := args[0] + + appMetadata, scopeDevices, _, err := ResolveScopeWithValidation(scope) + if err != nil { + return err + } + + // Enable each device using SDK + verbose := GetVerbose() + client := GetNotehubClient() + ctx, err := GetNotehubContext() + if err != nil { + return err + } + + for _, deviceUID := range scopeDevices { + _, err := client.DeviceAPI.EnableDevice(ctx, appMetadata.App.UID, deviceUID).Execute() + if err != nil { + return fmt.Errorf("failed to enable device %s: %w", deviceUID, err) + } + if verbose { + fmt.Printf("Device %s enabled\n", deviceUID) + } + } + + fmt.Printf("Successfully enabled %d device(s)\n", len(scopeDevices)) + return nil + }, +} + +// deviceDisableCmd represents the device disable command +var deviceDisableCmd = &cobra.Command{ + Use: "disable [scope]", + Short: "Disable one or more devices", + Long: `Disable one or more devices in a Notehub project, preventing them from communicating with Notehub. + +Scope Formats: + dev:xxxx Single device UID + imei:xxxx Device by IMEI + fleet:xxxx All devices in fleet (by UID) + production All devices in named fleet + @fleet-name All devices in fleet (indirection) + @ All devices in project + @devices.txt Device UIDs from file (one per line) + dev:aaa,dev:bbb Multiple scopes (comma-separated) + +Examples: + # Disable a single device + notehub device disable dev:864475046552567 + + # Disable all devices in a fleet + notehub device disable @production + + # Disable all devices in project + notehub device disable @ + + # Disable devices from a file + notehub device disable @devices.txt`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + GetCredentials() // Validates and exits if not authenticated + + scope := args[0] + + appMetadata, scopeDevices, _, err := ResolveScopeWithValidation(scope) + if err != nil { + return err + } + + // Disable each device using SDK + verbose := GetVerbose() + client := GetNotehubClient() + ctx, err := GetNotehubContext() + if err != nil { + return err + } + + for _, deviceUID := range scopeDevices { + _, err := client.DeviceAPI.DisableDevice(ctx, appMetadata.App.UID, deviceUID).Execute() + if err != nil { + return fmt.Errorf("failed to disable device %s: %w", deviceUID, err) + } + if verbose { + fmt.Printf("Device %s disabled\n", deviceUID) + } + } + + fmt.Printf("Successfully disabled %d device(s)\n", len(scopeDevices)) + return nil + }, +} + +// deviceMoveCmd represents the device move command +var deviceMoveCmd = &cobra.Command{ + Use: "move [scope] [fleet-uid-or-name]", + Short: "Move devices to a fleet", + Long: `Move one or more devices to a fleet. If a device is not in any fleet, it will be assigned. +If a device is already in a fleet, it will be moved to the new fleet. + +Scope Formats: + dev:xxxx Single device UID + imei:xxxx Device by IMEI + fleet:xxxx All devices in fleet (by UID) + production All devices in named fleet + @fleet-name All devices in fleet (indirection) + @ All devices in project + @devices.txt Device UIDs from file (one per line) + dev:aaa,dev:bbb Multiple scopes (comma-separated) + +Examples: + # Move a single device to a fleet + notehub device move dev:864475046552567 production + + # Move a device to a fleet by UID + notehub device move dev:864475046552567 fleet:xxxx + + # Move all devices from one fleet to another + notehub device move @old-fleet new-fleet + + # Move devices from a file to a fleet + notehub device move @devices.txt production`, + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + GetCredentials() // Validates and exits if not authenticated + + scope := args[0] + targetFleetIdentifier := args[1] + + appMetadata, scopeDevices, _, err := ResolveScopeWithValidation(scope) + if err != nil { + return err + } + + // Find the target fleet by UID or name + var targetFleetUID string + if strings.HasPrefix(targetFleetIdentifier, "fleet:") { + targetFleetUID = targetFleetIdentifier + } else { + // Search for fleet by name + found := false + for _, fleet := range appMetadata.Fleets { + if fleet.Name == targetFleetIdentifier { + targetFleetUID = fleet.UID + found = true + break + } + } + if !found { + return fmt.Errorf("fleet '%s' not found in project", targetFleetIdentifier) + } + } + + verbose := GetVerbose() + client := GetNotehubClient() + ctx, err := GetNotehubContext() + if err != nil { + return err + } + + // Move each device to the target fleet using SDK + for _, deviceUID := range scopeDevices { + // First, get the device's current fleets + currentFleets, _, err := client.ProjectAPI.GetDeviceFleets(ctx, appMetadata.App.UID, deviceUID).Execute() + if err != nil { + return fmt.Errorf("failed to get current fleets for device %s: %w", deviceUID, err) + } + + // Remove device from all current fleets if it has any + if currentFleets.Fleets != nil && len(currentFleets.Fleets) > 0 { + currentFleetUIDs := make([]string, len(currentFleets.Fleets)) + for i, fleet := range currentFleets.Fleets { + currentFleetUIDs[i] = fleet.Uid + } + + deleteReq := notehub.NewDeleteDeviceFromFleetsRequest(currentFleetUIDs) + _, _, err = client.ProjectAPI.DeleteDeviceFromFleets(ctx, appMetadata.App.UID, deviceUID). + DeleteDeviceFromFleetsRequest(*deleteReq). + Execute() + if err != nil { + return fmt.Errorf("failed to remove device %s from current fleets: %w", deviceUID, err) + } + if verbose { + fmt.Printf("Device %s removed from %d fleet(s)\n", deviceUID, len(currentFleetUIDs)) + } + } + + // Add device to the target fleet + addReq := notehub.NewAddDeviceToFleetsRequest([]string{targetFleetUID}) + _, _, err = client.ProjectAPI.AddDeviceToFleets(ctx, appMetadata.App.UID, deviceUID). + AddDeviceToFleetsRequest(*addReq). + Execute() + if err != nil { + return fmt.Errorf("failed to move device %s to fleet: %w", deviceUID, err) + } + if verbose { + fmt.Printf("Device %s moved to fleet %s\n", deviceUID, targetFleetUID) + } + } + + fmt.Printf("Successfully moved %d device(s) to fleet %s\n", len(scopeDevices), targetFleetUID) + return nil + }, +} + +// deviceHealthCmd represents the device health command +var deviceHealthCmd = &cobra.Command{ + Use: "health [device-uid]", + Short: "Get device health log", + Long: `Get the health log for a specific device, showing boot events, DFU completions, and other health-related information. + +Examples: + # Get health log for a device + notehub device health dev:864475046552567 + + # Get health log with JSON output + notehub device health dev:864475046552567 --json + + # Get health log with pretty JSON + notehub device health dev:864475046552567 --pretty`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + GetCredentials() // Validates and exits if not authenticated + + deviceUID := args[0] + + // Get project UID (from config or --project flag) + projectUID := GetProject() + if projectUID == "" { + return fmt.Errorf("no project set. Use 'notehub project set ' or provide --project flag") + } + + // Get device health log using SDK + client := GetNotehubClient() + ctx, err := GetNotehubContext() + if err != nil { + return err + } + + healthLogRsp, _, err := client.DeviceAPI.GetDeviceHealthLog(ctx, projectUID, deviceUID).Execute() + if err != nil { + return fmt.Errorf("failed to get device health log: %w", err) + } + + // Handle JSON output + if GetJson() || GetPretty() { + var output []byte + var err error + if GetPretty() { + output, err = note.JSONMarshalIndent(healthLogRsp, "", " ") + } else { + output, err = note.JSONMarshal(healthLogRsp) + } + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Printf("%s\n", output) + return nil + } + + if len(healthLogRsp.HealthLog) == 0 { + fmt.Println("No health log entries found for this device.") + return nil + } + + // Display health log in human-readable format + fmt.Printf("\nHealth Log for Device: %s\n", deviceUID) + fmt.Printf("================================\n\n") + + for _, entry := range healthLogRsp.HealthLog { + alertMarker := " " + if entry.Alert { + alertMarker = "!" + } + fmt.Printf("[%s] %s %s\n", entry.When.Format("2006-01-02 15:04:05 MST"), alertMarker, entry.Text) + } + + fmt.Printf("\nTotal entries: %d\n", len(healthLogRsp.HealthLog)) + fmt.Println() + + return nil + }, +} + +// deviceSessionCmd represents the device session command +var deviceSessionCmd = &cobra.Command{ + Use: "session [device-uid]", + Short: "Get device session log", + Long: `Get the session log for a specific device, showing connection history, network information, and session statistics. + +Examples: + # Get session log for a device + notehub device session dev:864475046552567 + + # Get session log with JSON output + notehub device session dev:864475046552567 --json + + # Get session log with pretty JSON + notehub device session dev:864475046552567 --pretty`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + GetCredentials() // Validates and exits if not authenticated + + deviceUID := args[0] + + // Get project UID (from config or --project flag) + projectUID := GetProject() + if projectUID == "" { + return fmt.Errorf("no project set. Use 'notehub project set ' or provide --project flag") + } + + // Get device sessions using SDK + client := GetNotehubClient() + ctx, err := GetNotehubContext() + if err != nil { + return err + } + + sessionsRsp, _, err := client.DeviceAPI.GetDeviceSessions(ctx, projectUID, deviceUID).Execute() + if err != nil { + return fmt.Errorf("failed to get device sessions: %w", err) + } + + // Handle JSON output + if GetJson() || GetPretty() { + var output []byte + var err error + if GetPretty() { + output, err = note.JSONMarshalIndent(sessionsRsp, "", " ") + } else { + output, err = note.JSONMarshal(sessionsRsp) + } + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Printf("%s\n", output) + return nil + } + + if len(sessionsRsp.Sessions) == 0 { + fmt.Println("No sessions found for this device.") + return nil + } + + // Display sessions in human-readable format + fmt.Printf("\nSession Log for Device: %s\n", deviceUID) + fmt.Printf("=================================\n\n") + + for i, session := range sessionsRsp.Sessions { + if i > 0 { + fmt.Println("---") + } + + // Session ID and timing + if session.Session != nil { + fmt.Printf("Session: %s\n", *session.Session) + } + if session.When != nil && *session.When > 0 { + sessionTime := time.Unix(*session.When, 0) + fmt.Printf(" Time: %s\n", sessionTime.Format("2006-01-02 15:04:05 MST")) + } + + // Session status + if session.WhySessionOpened != nil && *session.WhySessionOpened != "" { + fmt.Printf(" Opened: %s\n", *session.WhySessionOpened) + } + if session.WhySessionClosed != nil && *session.WhySessionClosed != "" { + fmt.Printf(" Closed: %s\n", *session.WhySessionClosed) + } + + // Network information + if (session.Rat != nil && *session.Rat != "") || (session.Bearer != nil && *session.Bearer != "") { + if session.Rat != nil { + fmt.Printf(" Network: %s", *session.Rat) + } + if session.Bearer != nil && *session.Bearer != "" { + fmt.Printf(" (%s)", *session.Bearer) + } + fmt.Println() + } + + // Signal quality + if session.Bars != nil && *session.Bars > 0 { + fmt.Printf(" Signal: %d bars", *session.Bars) + if session.Rssi != nil && *session.Rssi != 0 { + fmt.Printf(" (RSSI: %d)", *session.Rssi) + } + fmt.Println() + } + + // Location + if session.Tower != nil && session.Tower.N != nil && *session.Tower.N != "" { + fmt.Printf(" Location: %s", *session.Tower.N) + if session.Tower.C != nil && *session.Tower.C != "" { + fmt.Printf(", %s", *session.Tower.C) + } + fmt.Println() + } + + // Device status + if session.Voltage != nil && *session.Voltage > 0 { + fmt.Printf(" Voltage: %.3fV", *session.Voltage) + if session.Temp != nil && *session.Temp > 0 { + fmt.Printf(", Temp: %.1f°C", *session.Temp) + } + fmt.Println() + } + + // Session stats + if session.Events != nil && *session.Events > 0 { + fmt.Printf(" Events: %d", *session.Events) + if session.Tls != nil && *session.Tls { + fmt.Printf(" (TLS)") + } + fmt.Println() + } + + // Data transfer + if session.Period != nil { + if (session.Period.BytesSent != nil && *session.Period.BytesSent > 0) || + (session.Period.BytesRcvd != nil && *session.Period.BytesRcvd > 0) { + var sent, rcvd int64 + if session.Period.BytesSent != nil { + sent = *session.Period.BytesSent + } + if session.Period.BytesRcvd != nil { + rcvd = *session.Period.BytesRcvd + } + fmt.Printf(" Data: sent %d bytes, received %d bytes", sent, rcvd) + if session.Period.Duration != nil && *session.Period.Duration > 0 { + fmt.Printf(" (duration: %ds)", *session.Period.Duration) + } + fmt.Println() + } + } + } + + fmt.Printf("\nTotal sessions: %d", len(sessionsRsp.Sessions)) + if sessionsRsp.HasMore { + fmt.Printf(" (showing first page)") + } + fmt.Println() + fmt.Println() + + return nil + }, +} + +func init() { + rootCmd.AddCommand(deviceCmd) + deviceCmd.AddCommand(deviceListCmd) + deviceCmd.AddCommand(deviceEnableCmd) + deviceCmd.AddCommand(deviceDisableCmd) + deviceCmd.AddCommand(deviceMoveCmd) + deviceCmd.AddCommand(deviceHealthCmd) + deviceCmd.AddCommand(deviceSessionCmd) +} diff --git a/notehub/cmd/dfu.go b/notehub/cmd/dfu.go new file mode 100644 index 0000000..e8bebd2 --- /dev/null +++ b/notehub/cmd/dfu.go @@ -0,0 +1,513 @@ +// Copyright 2025 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package cmd + +import ( + "fmt" + "strings" + + "github.com/blues/note-go/note" + notehub "github.com/blues/notehub-go" + "github.com/spf13/cobra" +) + +// dfuCmd represents the dfu command +var dfuCmd = &cobra.Command{ + Use: "dfu", + Short: "Manage device firmware updates", + Long: `Commands for scheduling and managing firmware updates for Notecards and host MCUs.`, +} + +// dfuUpdateCmd represents the dfu update command +var dfuUpdateCmd = &cobra.Command{ + Use: "update [firmware-type] [filename] [scope]", + Short: "Schedule a firmware update", + Long: `Schedule a firmware update for devices. Firmware type must be either 'host' or 'notecard'. + +The filename should match a firmware file that has been uploaded to your Notehub project. + +Scope Formats: + dev:xxxx Single device UID + imei:xxxx Device by IMEI + fleet:xxxx All devices in fleet (by UID) + production All devices in named fleet + @fleet-name All devices in fleet (indirection) + @ All devices in project + @devices.txt Device UIDs from file (one per line) + dev:aaa,dev:bbb Multiple scopes (comma-separated) + +Additional filters can be used to narrow down the scope: + --location Filter by location + --notecard-firmware Filter by Notecard firmware version + --host-firmware Filter by host firmware version + --product Filter by product UID + --sku Filter by SKU + --tag Filter by device tags (comma-separated) + --serial Filter by serial numbers (comma-separated) + +Examples: + # Schedule notecard firmware update for a specific device + notehub dfu update notecard notecard-6.2.1.bin dev:864475046552567 + + # Schedule host firmware update for all devices in a fleet + notehub dfu update host app-v1.2.3.bin @production + + # Schedule update for multiple devices + notehub dfu update notecard notecard-6.2.1.bin dev:aaa,dev:bbb,dev:ccc + + # Schedule update for all devices in project + notehub dfu update notecard notecard-6.2.1.bin @ + + # Schedule update for devices from a file + notehub dfu update host app-v1.2.3.bin @devices.txt + + # Schedule update with additional filters + notehub dfu update notecard notecard-6.2.1.bin @production --sku NOTE-WBEX`, + Args: cobra.ExactArgs(3), + RunE: func(cmd *cobra.Command, args []string) error { + GetCredentials() // Validates and exits if not authenticated + + firmwareType := args[0] + filename := args[1] + scope := args[2] + + // Validate firmware type + if firmwareType != "host" && firmwareType != "notecard" { + return fmt.Errorf("firmware type must be 'host' or 'notecard', got '%s'", firmwareType) + } + + // Resolve scope to device UIDs + appMetadata, scopeDevices, _, err := ResolveScopeWithValidation(scope) + if err != nil { + return err + } + + verbose := GetVerbose() + + // Get additional filter flags + tags, _ := cmd.Flags().GetString("tag") + serialNumbers, _ := cmd.Flags().GetString("serial") + location, _ := cmd.Flags().GetString("location") + notecardFirmware, _ := cmd.Flags().GetString("notecard-firmware") + hostFirmware, _ := cmd.Flags().GetString("host-firmware") + productUID, _ := cmd.Flags().GetString("product") + sku, _ := cmd.Flags().GetString("sku") + + // Build request body + dfuRequest := notehub.NewDfuActionRequest() + dfuRequest.SetFilename(filename) + + // Get SDK client + client := GetNotehubClient() + ctx, err := GetNotehubContext() + if err != nil { + return err + } + + // Build request with SDK + req := client.ProjectAPI.PerformDfuAction(ctx, appMetadata.App.UID, firmwareType, "update"). + DfuActionRequest(*dfuRequest) + + // Add device UIDs + if len(scopeDevices) > 0 { + req = req.DeviceUID(scopeDevices) + } + + // Add optional filters + if tags != "" { + req = req.Tag(strings.Split(tags, ",")) + } + if serialNumbers != "" { + req = req.SerialNumber(strings.Split(serialNumbers, ",")) + } + if location != "" { + req = req.Location([]string{location}) + } + if notecardFirmware != "" { + req = req.NotecardFirmware([]string{notecardFirmware}) + } + if hostFirmware != "" { + req = req.HostFirmware([]string{hostFirmware}) + } + if productUID != "" { + req = req.ProductUID([]string{productUID}) + } + if sku != "" { + req = req.Sku([]string{sku}) + } + + // Execute the DFU update + _, err = req.Execute() + if err != nil { + return fmt.Errorf("failed to schedule firmware update: %w", err) + } + + fmt.Printf("\nFirmware update scheduled successfully!\n\n") + fmt.Printf("Firmware Type: %s\n", firmwareType) + fmt.Printf("Filename: %s\n", filename) + fmt.Printf("Scope: %s\n", scope) + fmt.Printf("Target Devices: %d device(s)\n", len(scopeDevices)) + if verbose && len(scopeDevices) > 0 { + fmt.Printf("Device UIDs: %s\n", strings.Join(scopeDevices, ",")) + } + if tags != "" { + fmt.Printf("Additional Tag Filter: %s\n", tags) + } + if serialNumbers != "" { + fmt.Printf("Additional Serial Filter: %s\n", serialNumbers) + } + if location != "" { + fmt.Printf("Additional Location Filter: %s\n", location) + } + if sku != "" { + fmt.Printf("Additional SKU Filter: %s\n", sku) + } + fmt.Println() + + return nil + }, +} + +// dfuCancelCmd represents the dfu cancel command +var dfuCancelCmd = &cobra.Command{ + Use: "cancel [firmware-type] [scope]", + Short: "Cancel pending firmware updates", + Long: `Cancel pending firmware updates for devices. Firmware type must be either 'host' or 'notecard'. + +Scope Formats: + dev:xxxx Single device UID + imei:xxxx Device by IMEI + fleet:xxxx All devices in fleet (by UID) + production All devices in named fleet + @fleet-name All devices in fleet (indirection) + @ All devices in project + @devices.txt Device UIDs from file (one per line) + dev:aaa,dev:bbb Multiple scopes (comma-separated) + +Additional filters can be used to narrow down the scope: + --tag Filter by device tags (comma-separated) + --serial Filter by serial numbers (comma-separated) + +Examples: + # Cancel notecard firmware update for a specific device + notehub dfu cancel notecard dev:864475046552567 + + # Cancel host firmware updates for all devices in a fleet + notehub dfu cancel host @production + + # Cancel updates for multiple devices + notehub dfu cancel notecard dev:aaa,dev:bbb,dev:ccc + + # Cancel updates for all devices in project + notehub dfu cancel notecard @ + + # Cancel updates for devices from a file + notehub dfu cancel host @devices.txt`, + Args: cobra.ExactArgs(2), + RunE: func(cmd *cobra.Command, args []string) error { + GetCredentials() // Validates and exits if not authenticated + + firmwareType := args[0] + scope := args[1] + + // Validate firmware type + if firmwareType != "host" && firmwareType != "notecard" { + return fmt.Errorf("firmware type must be 'host' or 'notecard', got '%s'", firmwareType) + } + + // Resolve scope to device UIDs + appMetadata, scopeDevices, _, err := ResolveScopeWithValidation(scope) + if err != nil { + return err + } + + verbose := GetVerbose() + + // Get additional filter flags + tags, _ := cmd.Flags().GetString("tag") + serialNumbers, _ := cmd.Flags().GetString("serial") + + // Get SDK client + client := GetNotehubClient() + ctx, err := GetNotehubContext() + if err != nil { + return err + } + + // Build cancel request with SDK + req := client.ProjectAPI.PerformDfuAction(ctx, appMetadata.App.UID, firmwareType, "cancel") + + // Add device UIDs + if len(scopeDevices) > 0 { + req = req.DeviceUID(scopeDevices) + } + + // Add optional filters + if tags != "" { + req = req.Tag(strings.Split(tags, ",")) + } + if serialNumbers != "" { + req = req.SerialNumber(strings.Split(serialNumbers, ",")) + } + + // Execute the DFU cancel + _, err = req.Execute() + if err != nil { + return fmt.Errorf("failed to cancel firmware update: %w", err) + } + + fmt.Printf("\nFirmware update cancelled successfully!\n\n") + fmt.Printf("Firmware Type: %s\n", firmwareType) + fmt.Printf("Scope: %s\n", scope) + fmt.Printf("Target Devices: %d device(s)\n", len(scopeDevices)) + if verbose && len(scopeDevices) > 0 { + fmt.Printf("Device UIDs: %s\n", strings.Join(scopeDevices, ",")) + } + if tags != "" { + fmt.Printf("Additional Tag Filter: %s\n", tags) + } + if serialNumbers != "" { + fmt.Printf("Additional Serial Filter: %s\n", serialNumbers) + } + fmt.Println() + + return nil + }, +} + +// dfuListCmd represents the dfu list command +var dfuListCmd = &cobra.Command{ + Use: "list", + Short: "List available firmware files", + Long: `List all firmware files available in the current project. + +You can filter by firmware type (host or notecard) and other criteria. + +Examples: + # List all firmware files + notehub dfu list + + # List only host firmware + notehub dfu list --type host + + # List only notecard firmware + notehub dfu list --type notecard + + # List with JSON output + notehub dfu list --pretty`, + RunE: func(cmd *cobra.Command, args []string) error { + GetCredentials() // Validates and exits if not authenticated + + // Get project UID (from config or --project flag) + projectUID := GetProject() + if projectUID == "" { + return fmt.Errorf("no project set. Use 'notehub project set ' or provide --project flag") + } + + // Get filter flags + firmwareType, _ := cmd.Flags().GetString("type") + productUID, _ := cmd.Flags().GetString("product") + version, _ := cmd.Flags().GetString("version") + target, _ := cmd.Flags().GetString("target") + filename, _ := cmd.Flags().GetString("filename") + unpublished, _ := cmd.Flags().GetBool("unpublished") + + // Get SDK client + client := GetNotehubClient() + ctx, err := GetNotehubContext() + if err != nil { + return err + } + + // Build request with SDK + req := client.ProjectAPI.GetFirmwareInfo(ctx, projectUID) + + // Add query parameters + if firmwareType != "" { + req = req.FirmwareType(firmwareType) + } + if productUID != "" { + req = req.Product(productUID) + } + if version != "" { + req = req.Version(version) + } + if target != "" { + req = req.Target(target) + } + if filename != "" { + req = req.Filename(filename) + } + if unpublished { + req = req.Unpublished(unpublished) + } + + // Get firmware list using SDK + firmwareList, _, err := req.Execute() + if err != nil { + return fmt.Errorf("failed to list firmware: %w", err) + } + + // Handle JSON output + if GetJson() || GetPretty() { + var output []byte + var err error + if GetPretty() { + output, err = note.JSONMarshalIndent(firmwareList, "", " ") + } else { + output, err = note.JSONMarshal(firmwareList) + } + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Printf("%s\n", output) + return nil + } + + if len(firmwareList) == 0 { + fmt.Println("No firmware files found.") + return nil + } + + // Display firmware in human-readable format + fmt.Printf("\nAvailable Firmware Files:\n") + fmt.Printf("=========================\n\n") + + // Group by type + hostFirmware := []notehub.FirmwareInfo{} + notecardFirmware := []notehub.FirmwareInfo{} + otherFirmware := []notehub.FirmwareInfo{} + + for _, fw := range firmwareList { + if fw.Type != nil && *fw.Type == "host" { + hostFirmware = append(hostFirmware, fw) + } else if fw.Type != nil && *fw.Type == "notecard" { + notecardFirmware = append(notecardFirmware, fw) + } else { + otherFirmware = append(otherFirmware, fw) + } + } + + // Display host firmware + if len(hostFirmware) > 0 { + fmt.Printf("Host Firmware (%d):\n", len(hostFirmware)) + fmt.Printf("------------------\n") + for _, fw := range hostFirmware { + if fw.Filename != nil { + fmt.Printf(" %s", *fw.Filename) + } + if fw.Version != nil && *fw.Version != "" { + fmt.Printf(" (v%s)", *fw.Version) + } + if fw.Published != nil && !*fw.Published { + fmt.Printf(" [unpublished]") + } + fmt.Println() + if fw.Description != nil && *fw.Description != "" { + fmt.Printf(" Description: %s\n", *fw.Description) + } + if fw.Built != nil && *fw.Built != "" { + fmt.Printf(" Built: %s\n", *fw.Built) + } + if fw.Target != nil && *fw.Target != "" { + fmt.Printf(" Target: %s\n", *fw.Target) + } + fmt.Println() + } + } + + // Display notecard firmware + if len(notecardFirmware) > 0 { + fmt.Printf("Notecard Firmware (%d):\n", len(notecardFirmware)) + fmt.Printf("----------------------\n") + for _, fw := range notecardFirmware { + if fw.Filename != nil { + fmt.Printf(" %s", *fw.Filename) + } + if fw.Version != nil && *fw.Version != "" { + fmt.Printf(" (v%s)", *fw.Version) + } + if fw.Published != nil && !*fw.Published { + fmt.Printf(" [unpublished]") + } + fmt.Println() + if fw.Description != nil && *fw.Description != "" { + fmt.Printf(" Description: %s\n", *fw.Description) + } + if fw.Built != nil && *fw.Built != "" { + fmt.Printf(" Built: %s\n", *fw.Built) + } + if fw.Target != nil && *fw.Target != "" { + fmt.Printf(" Target: %s\n", *fw.Target) + } + fmt.Println() + } + } + + // Display other firmware + if len(otherFirmware) > 0 { + fmt.Printf("Other Firmware (%d):\n", len(otherFirmware)) + fmt.Printf("-------------------\n") + for _, fw := range otherFirmware { + if fw.Filename != nil { + fmt.Printf(" %s", *fw.Filename) + } + if fw.Version != nil && *fw.Version != "" { + fmt.Printf(" (v%s)", *fw.Version) + } + if fw.Type != nil && *fw.Type != "" { + fmt.Printf(" [%s]", *fw.Type) + } + if fw.Published != nil && !*fw.Published { + fmt.Printf(" [unpublished]") + } + fmt.Println() + if fw.Description != nil && *fw.Description != "" { + fmt.Printf(" Description: %s\n", *fw.Description) + } + if fw.Built != nil && *fw.Built != "" { + fmt.Printf(" Built: %s\n", *fw.Built) + } + if fw.Target != nil && *fw.Target != "" { + fmt.Printf(" Target: %s\n", *fw.Target) + } + fmt.Println() + } + } + + fmt.Printf("Total firmware files: %d\n\n", len(firmwareList)) + + return nil + }, +} + +func init() { + rootCmd.AddCommand(dfuCmd) + dfuCmd.AddCommand(dfuListCmd) + dfuCmd.AddCommand(dfuUpdateCmd) + dfuCmd.AddCommand(dfuCancelCmd) + + // Add flags for dfu list + dfuListCmd.Flags().String("type", "", "Filter by firmware type (host or notecard)") + dfuListCmd.Flags().String("product", "", "Filter by product UID") + dfuListCmd.Flags().String("version", "", "Filter by version") + dfuListCmd.Flags().String("target", "", "Filter by target device") + dfuListCmd.Flags().String("filename", "", "Filter by filename") + dfuListCmd.Flags().Bool("unpublished", false, "Include unpublished firmware") + dfuListCmd.Flags().MarkHidden("unpublished") + + // Add flags for dfu update (additional filters beyond scope) + dfuUpdateCmd.Flags().String("tag", "", "Additional filter by device tags (comma-separated)") + dfuUpdateCmd.Flags().String("serial", "", "Additional filter by serial numbers (comma-separated)") + dfuUpdateCmd.Flags().String("location", "", "Additional filter by location") + dfuUpdateCmd.Flags().String("notecard-firmware", "", "Additional filter by Notecard firmware version") + dfuUpdateCmd.Flags().String("host-firmware", "", "Additional filter by host firmware version") + dfuUpdateCmd.Flags().String("product", "", "Additional filter by product UID") + dfuUpdateCmd.Flags().String("sku", "", "Additional filter by SKU") + + // Add flags for dfu cancel (additional filters beyond scope) + dfuCancelCmd.Flags().String("tag", "", "Additional filter by device tags (comma-separated)") + dfuCancelCmd.Flags().String("serial", "", "Additional filter by serial numbers (comma-separated)") +} diff --git a/notehub/cmd/explore.go b/notehub/cmd/explore.go new file mode 100644 index 0000000..870e60d --- /dev/null +++ b/notehub/cmd/explore.go @@ -0,0 +1,156 @@ +// Copyright 2025 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package cmd + +import ( + "fmt" + "sort" + + "github.com/blues/note-go/note" + "github.com/blues/note-go/notecard" + "github.com/blues/note-go/notehub" + "github.com/spf13/cobra" +) + +var ( + flagReserved bool +) + +// exploreCmd represents the explore command +var exploreCmd = &cobra.Command{ + Use: "explore", + Short: "Explore the contents of a device", + Long: `Explore the notefiles and notes on a device. + +By default, reserved notefiles are not shown. Use --reserved to include them. + +Example: + notehub explore --device dev:xxxx --pretty + notehub explore --scope @production --reserved`, + RunE: func(cmd *cobra.Command, args []string) error { + GetCredentials() // Validate credentials + + device := GetDevice() + if flagScope == "" && device == "" { + return fmt.Errorf("use --device to specify a device or --scope to specify multiple devices") + } + + // If scope is specified, iterate over multiple devices + if flagScope != "" { + appMetadata, scopeDevices, _, err := ResolveScopeWithValidation(flagScope) + if err != nil { + return err + } + + verbose := GetVerbose() + pretty := GetPretty() + + for _, deviceUID := range scopeDevices { + reqFlagDevice = deviceUID + err = exploreDevice(flagReserved, verbose, pretty) + if err != nil { + return err + } + } + + // Set the project for the request + reqFlagApp = appMetadata.App.UID + } else { + // Single device exploration + reqFlagDevice = device + reqFlagApp = GetProject() + err := exploreDevice(flagReserved, GetVerbose(), GetPretty()) + if err != nil { + return err + } + } + + return nil + }, +} + +func init() { + rootCmd.AddCommand(exploreCmd) + + exploreCmd.Flags().BoolVarP(&flagReserved, "reserved", "r", false, "Include reserved notefiles") + exploreCmd.Flags().StringVarP(&flagScope, "scope", "s", "", "Device scope (alternative to --device)") +} + +// Explore the contents of a device +// Note: This function intentionally uses V0 Notecard APIs (file.changes, note.changes) +// These are device-specific APIs for communicating with Notecard hardware, distinct from +// the Notehub project management APIs which have been migrated to V1 REST endpoints. +func exploreDevice(includeReserved bool, verbose bool, pretty bool) (err error) { + // Get the list of notefiles using file.changes API + req := notehub.HubRequest{} + req.Req = notecard.ReqFileChanges + req.Allow = includeReserved + var rsp notehub.HubRequest + rsp, err = hubTransactionRequest(req, verbose) + if err != nil { + return + } + + // Exit if no notefiles + fmt.Printf("%s\n", reqFlagDevice) + if rsp.FileInfo == nil || len(*rsp.FileInfo) == 0 { + fmt.Printf(" no notefiles\n") + return + } + + // Sort the notefiles + notefileIDs := []string{} + for notefileID := range *rsp.FileInfo { + notefileIDs = append(notefileIDs, notefileID) + } + sort.Strings(notefileIDs) + + // Iterate over each file + for _, notefileID := range notefileIDs { + fmt.Printf(" %s\n", notefileID) + + // Get the notes using note.changes API + req = notehub.HubRequest{} + req.Req = notecard.ReqNoteChanges + req.Allow = includeReserved + req.Deleted = true + req.NotefileID = notefileID + rsp, err = hubTransactionRequest(req, verbose) + if err != nil { + return + } + + // Exit if no notefiles + if rsp.Notes == nil || len(*rsp.Notes) == 0 { + continue + } + + // Show the notes + for noteID, n := range *rsp.Notes { + fmt.Printf(" %s", noteID) + if n.Deleted { + fmt.Printf(" (DELETED)") + } + fmt.Printf("\n") + if n.Body != nil { + prefix := " " + var bodyJSON []byte + if pretty { + bodyJSON, err = note.JSONMarshalIndent(*n.Body, prefix, " ") + } else { + bodyJSON, err = note.JSONMarshal(*n.Body) + } + if err == nil { + fmt.Printf("%s%s\n", prefix, string(bodyJSON)) + } + } + if n.Payload != nil { + fmt.Printf(" Payload: %d bytes\n", len(*n.Payload)) + } + } + } + + return +} diff --git a/notehub/cmd/fleet.go b/notehub/cmd/fleet.go new file mode 100644 index 0000000..00de9e3 --- /dev/null +++ b/notehub/cmd/fleet.go @@ -0,0 +1,532 @@ +// Copyright 2025 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package cmd + +import ( + "fmt" + + "github.com/blues/note-go/note" + notehub "github.com/blues/notehub-go" + "github.com/spf13/cobra" +) + +// fleetCmd represents the fleet command +var fleetCmd = &cobra.Command{ + Use: "fleet", + Short: "Manage Notehub fleets", + Long: `Commands for listing and managing fleets in Notehub projects.`, +} + +// fleetListCmd represents the fleet list command +var fleetListCmd = &cobra.Command{ + Use: "list", + Short: "List all fleets", + Long: `List all fleets in the current project or a specified project.`, + RunE: func(cmd *cobra.Command, args []string) error { + GetCredentials() // Validates and exits if not authenticated + + // Get project UID (from config or --project flag) + projectUID := GetProject() + if projectUID == "" { + return fmt.Errorf("no project set. Use 'notehub project set ' or provide --project flag") + } + + // Get fleets using SDK + client := GetNotehubClient() + ctx, err := GetNotehubContext() + if err != nil { + return err + } + + fleetsRsp, _, err := client.ProjectAPI.GetFleets(ctx, projectUID).Execute() + if err != nil { + return fmt.Errorf("failed to list fleets: %w", err) + } + + // Handle JSON output + if GetJson() || GetPretty() { + var output []byte + var err error + if GetPretty() { + output, err = note.JSONMarshalIndent(fleetsRsp, "", " ") + } else { + output, err = note.JSONMarshal(fleetsRsp) + } + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Printf("%s\n", output) + return nil + } + + if len(fleetsRsp.Fleets) == 0 { + fmt.Println("No fleets found in this project.") + return nil + } + + // Display fleets in human-readable format + fmt.Printf("\nFleets in Project:\n") + fmt.Printf("==================\n\n") + + for _, fleet := range fleetsRsp.Fleets { + fmt.Printf("Fleet: %s\n", fleet.Label) + fmt.Printf(" UID: %s\n", fleet.Uid) + if !fleet.Created.IsZero() { + fmt.Printf(" Created: %s\n", fleet.Created.Format("2006-01-02 15:04:05 MST")) + } + if fleet.HasSmartRule() { + fmt.Printf(" Smart Rule: %s\n", *fleet.SmartRule) + } + if fleet.HasConnectivityAssurance() { + status := "disabled" + if ca := fleet.ConnectivityAssurance.Get(); ca != nil && ca.Enabled.IsSet() { + if enabled := ca.Enabled.Get(); enabled != nil && *enabled { + status = "enabled" + } + } + fmt.Printf(" Connectivity Assurance: %s\n", status) + } + if fleet.HasWatchdogMins() && *fleet.WatchdogMins > 0 { + fmt.Printf(" Watchdog: %d minutes\n", *fleet.WatchdogMins) + } + fmt.Println() + } + + fmt.Printf("Total fleets: %d\n\n", len(fleetsRsp.Fleets)) + + return nil + }, +} + +// fleetGetCmd represents the fleet get command +var fleetGetCmd = &cobra.Command{ + Use: "get [fleet-uid-or-name]", + Short: "Get details about a specific fleet", + Long: `Get detailed information about a specific fleet by UID or name.`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + GetCredentials() // Validates and exits if not authenticated + + fleetIdentifier := args[0] + + // Get project UID (from config or --project flag) + projectUID := GetProject() + if projectUID == "" { + return fmt.Errorf("no project set. Use 'notehub project set ' or provide --project flag") + } + + // Get SDK client + client := GetNotehubClient() + ctx, err := GetNotehubContext() + if err != nil { + return err + } + + // First, try to use it directly as a UID + var selectedFleet *notehub.Fleet + fleet, resp, err := client.ProjectAPI.GetFleet(ctx, projectUID, fleetIdentifier).Execute() + + // If that failed or returned 404, it might be a fleet name - fetch all fleets and search + if err != nil || (resp != nil && resp.StatusCode == 404) { + fleetsRsp, _, err := client.ProjectAPI.GetFleets(ctx, projectUID).Execute() + if err != nil { + return fmt.Errorf("failed to list fleets: %w", err) + } + + // Search for fleet by name (exact match) + found := false + for _, f := range fleetsRsp.Fleets { + if f.Label == fleetIdentifier { + selectedFleet = &f + found = true + break + } + } + + if !found { + return fmt.Errorf("fleet '%s' not found in project", fleetIdentifier) + } + } else { + selectedFleet = fleet + } + + // Handle JSON output + if GetJson() || GetPretty() { + var output []byte + var err error + if GetPretty() { + output, err = note.JSONMarshalIndent(selectedFleet, "", " ") + } else { + output, err = note.JSONMarshal(selectedFleet) + } + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Printf("%s\n", output) + return nil + } + + // Display fleet in human-readable format + fmt.Printf("\nFleet Details:\n") + fmt.Printf("==============\n\n") + fmt.Printf("Name: %s\n", selectedFleet.Label) + fmt.Printf("UID: %s\n", selectedFleet.Uid) + if !selectedFleet.Created.IsZero() { + fmt.Printf("Created: %s\n", selectedFleet.Created.Format("2006-01-02 15:04:05 MST")) + } + if selectedFleet.HasSmartRule() { + fmt.Printf("Smart Rule: %s\n", *selectedFleet.SmartRule) + } + if selectedFleet.HasConnectivityAssurance() { + status := "disabled" + if ca := selectedFleet.ConnectivityAssurance.Get(); ca != nil && ca.Enabled.IsSet() { + if enabled := ca.Enabled.Get(); enabled != nil && *enabled { + status = "enabled" + } + } + fmt.Printf("Connectivity Assurance: %s\n", status) + } + if selectedFleet.HasWatchdogMins() && *selectedFleet.WatchdogMins > 0 { + fmt.Printf("Watchdog: %d minutes\n", *selectedFleet.WatchdogMins) + } + + // Display environment variables if any + if selectedFleet.HasEnvironmentVariables() { + envVars := selectedFleet.GetEnvironmentVariables() + if len(envVars) > 0 { + fmt.Printf("\nEnvironment Variables:\n") + for key, value := range envVars { + fmt.Printf(" %s: %s\n", key, value) + } + } + } + + fmt.Println() + + return nil + }, +} + +// fleetCreateCmd represents the fleet create command +var fleetCreateCmd = &cobra.Command{ + Use: "create [name]", + Short: "Create a new fleet", + Long: `Create a new fleet in the current project.`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + GetCredentials() // Validates and exits if not authenticated + + fleetName := args[0] + + // Get project UID (from config or --project flag) + projectUID := GetProject() + if projectUID == "" { + return fmt.Errorf("no project set. Use 'notehub project set ' or provide --project flag") + } + + // Get optional flags + smartRule, _ := cmd.Flags().GetString("smart-rule") + connectivityAssurance, _ := cmd.Flags().GetBool("connectivity-assurance") + + // Get SDK client + client := GetNotehubClient() + ctx, err := GetNotehubContext() + if err != nil { + return err + } + + // Build create request using SDK + createReq := notehub.NewCreateFleetRequest() + createReq.SetLabel(fleetName) + + if smartRule != "" { + createReq.SetSmartRule(smartRule) + } + + if cmd.Flags().Changed("connectivity-assurance") { + ca := notehub.NewFleetConnectivityAssurance() + ca.Enabled.Set(&connectivityAssurance) + createReq.SetConnectivityAssurance(*ca) + } + + // Create fleet using SDK + createdFleet, _, err := client.ProjectAPI.CreateFleet(ctx, projectUID). + CreateFleetRequest(*createReq). + Execute() + if err != nil { + return fmt.Errorf("failed to create fleet: %w", err) + } + + // Handle JSON output + if GetJson() || GetPretty() { + var output []byte + var err error + if GetPretty() { + output, err = note.JSONMarshalIndent(createdFleet, "", " ") + } else { + output, err = note.JSONMarshal(createdFleet) + } + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Printf("%s\n", output) + return nil + } + + // Display success message + fmt.Printf("\nFleet created successfully!\n\n") + fmt.Printf("Name: %s\n", createdFleet.Label) + fmt.Printf("UID: %s\n", createdFleet.Uid) + if !createdFleet.Created.IsZero() { + fmt.Printf("Created: %s\n", createdFleet.Created.Format("2006-01-02 15:04:05 MST")) + } + if createdFleet.HasSmartRule() { + fmt.Printf("Smart Rule: %s\n", *createdFleet.SmartRule) + } + if createdFleet.HasConnectivityAssurance() { + status := "disabled" + if ca := createdFleet.ConnectivityAssurance.Get(); ca != nil && ca.Enabled.IsSet() { + if enabled := ca.Enabled.Get(); enabled != nil && *enabled { + status = "enabled" + } + } + fmt.Printf("Connectivity Assurance: %s\n", status) + } + fmt.Println() + + return nil + }, +} + +// fleetDeleteCmd represents the fleet delete command +var fleetDeleteCmd = &cobra.Command{ + Use: "delete [fleet-uid-or-name]", + Short: "Delete a fleet", + Long: `Delete a fleet from the current project.`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + GetCredentials() // Validates and exits if not authenticated + + fleetIdentifier := args[0] + + // Get project UID (from config or --project flag) + projectUID := GetProject() + if projectUID == "" { + return fmt.Errorf("no project set. Use 'notehub project set ' or provide --project flag") + } + + // Get SDK client + client := GetNotehubClient() + ctx, err := GetNotehubContext() + if err != nil { + return err + } + + // Determine the fleet UID + var fleetUID string + var fleetName string + + // First, try to use it directly as a UID + fleet, resp, err := client.ProjectAPI.GetFleet(ctx, projectUID, fleetIdentifier).Execute() + + if err == nil && resp != nil && resp.StatusCode != 404 { + // It's a valid UID + fleetUID = fleet.Uid + fleetName = fleet.Label + } else { + // Try to find by name + fleetsRsp, _, err := client.ProjectAPI.GetFleets(ctx, projectUID).Execute() + if err != nil { + return fmt.Errorf("failed to list fleets: %w", err) + } + + // Search for fleet by name (exact match) + found := false + for _, f := range fleetsRsp.Fleets { + if f.Label == fleetIdentifier { + fleetUID = f.Uid + fleetName = f.Label + found = true + break + } + } + + if !found { + return fmt.Errorf("fleet '%s' not found in project", fleetIdentifier) + } + } + + // Delete fleet using SDK + _, err = client.ProjectAPI.DeleteFleet(ctx, projectUID, fleetUID).Execute() + if err != nil { + return fmt.Errorf("failed to delete fleet: %w", err) + } + + fmt.Printf("\nFleet '%s' (UID: %s) deleted successfully.\n\n", fleetName, fleetUID) + + return nil + }, +} + +// fleetUpdateCmd represents the fleet update command +var fleetUpdateCmd = &cobra.Command{ + Use: "update [fleet-uid-or-name]", + Short: "Update a fleet", + Long: `Update a fleet's properties such as name, smart rule, connectivity assurance, or watchdog timer.`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + GetCredentials() // Validates and exits if not authenticated + + fleetIdentifier := args[0] + + // Get project UID (from config or --project flag) + projectUID := GetProject() + if projectUID == "" { + return fmt.Errorf("no project set. Use 'notehub project set ' or provide --project flag") + } + + // Get optional flags + newName, _ := cmd.Flags().GetString("name") + smartRule, _ := cmd.Flags().GetString("smart-rule") + connectivityAssurance, _ := cmd.Flags().GetBool("connectivity-assurance") + watchdogMins, _ := cmd.Flags().GetInt("watchdog-mins") + + // Check if any update flags were provided + if !cmd.Flags().Changed("name") && + !cmd.Flags().Changed("smart-rule") && + !cmd.Flags().Changed("connectivity-assurance") && + !cmd.Flags().Changed("watchdog-mins") { + return fmt.Errorf("no update flags provided. Use --name, --smart-rule, --connectivity-assurance, or --watchdog-mins") + } + + // Get SDK client + client := GetNotehubClient() + ctx, err := GetNotehubContext() + if err != nil { + return err + } + + // Determine the fleet UID + var fleetUID string + + // First, try to use it directly as a UID + fleet, resp, err := client.ProjectAPI.GetFleet(ctx, projectUID, fleetIdentifier).Execute() + + if err == nil && resp != nil && resp.StatusCode != 404 { + // It's a valid UID + fleetUID = fleet.Uid + } else { + // Try to find by name + fleetsRsp, _, err := client.ProjectAPI.GetFleets(ctx, projectUID).Execute() + if err != nil { + return fmt.Errorf("failed to list fleets: %w", err) + } + + // Search for fleet by name (exact match) + found := false + for _, f := range fleetsRsp.Fleets { + if f.Label == fleetIdentifier { + fleetUID = f.Uid + found = true + break + } + } + + if !found { + return fmt.Errorf("fleet '%s' not found in project", fleetIdentifier) + } + } + + // Build update request using SDK + updateReq := notehub.NewUpdateFleetRequest() + + if cmd.Flags().Changed("name") { + updateReq.SetLabel(newName) + } + + if cmd.Flags().Changed("smart-rule") { + updateReq.SetSmartRule(smartRule) + } + + if cmd.Flags().Changed("connectivity-assurance") { + ca := notehub.NewFleetConnectivityAssurance() + ca.Enabled.Set(&connectivityAssurance) + updateReq.SetConnectivityAssurance(*ca) + } + + if cmd.Flags().Changed("watchdog-mins") { + updateReq.SetWatchdogMins(int64(watchdogMins)) + } + + // Update fleet using SDK + updatedFleet, _, err := client.ProjectAPI.UpdateFleet(ctx, projectUID, fleetUID). + UpdateFleetRequest(*updateReq). + Execute() + if err != nil { + return fmt.Errorf("failed to update fleet: %w", err) + } + + // Handle JSON output + if GetJson() || GetPretty() { + var output []byte + var err error + if GetPretty() { + output, err = note.JSONMarshalIndent(updatedFleet, "", " ") + } else { + output, err = note.JSONMarshal(updatedFleet) + } + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Printf("%s\n", output) + return nil + } + + // Display success message + fmt.Printf("\nFleet updated successfully!\n\n") + fmt.Printf("Name: %s\n", updatedFleet.Label) + fmt.Printf("UID: %s\n", updatedFleet.Uid) + if !updatedFleet.Created.IsZero() { + fmt.Printf("Created: %s\n", updatedFleet.Created.Format("2006-01-02 15:04:05 MST")) + } + if updatedFleet.HasSmartRule() { + fmt.Printf("Smart Rule: %s\n", *updatedFleet.SmartRule) + } + if updatedFleet.HasConnectivityAssurance() { + status := "disabled" + if ca := updatedFleet.ConnectivityAssurance.Get(); ca != nil && ca.Enabled.IsSet() { + if enabled := ca.Enabled.Get(); enabled != nil && *enabled { + status = "enabled" + } + } + fmt.Printf("Connectivity Assurance: %s\n", status) + } + if updatedFleet.HasWatchdogMins() && *updatedFleet.WatchdogMins > 0 { + fmt.Printf("Watchdog: %d minutes\n", *updatedFleet.WatchdogMins) + } + fmt.Println() + + return nil + }, +} + +func init() { + rootCmd.AddCommand(fleetCmd) + fleetCmd.AddCommand(fleetListCmd) + fleetCmd.AddCommand(fleetGetCmd) + fleetCmd.AddCommand(fleetCreateCmd) + fleetCmd.AddCommand(fleetDeleteCmd) + fleetCmd.AddCommand(fleetUpdateCmd) + + // Add flags for fleet create + fleetCreateCmd.Flags().String("smart-rule", "", "JSONata expression for dynamic fleet membership") + fleetCreateCmd.Flags().Bool("connectivity-assurance", false, "Enable connectivity assurance for this fleet") + + // Add flags for fleet update + fleetUpdateCmd.Flags().String("name", "", "New name for the fleet") + fleetUpdateCmd.Flags().String("smart-rule", "", "JSONata expression for dynamic fleet membership") + fleetUpdateCmd.Flags().Bool("connectivity-assurance", false, "Enable or disable connectivity assurance") + fleetUpdateCmd.Flags().Int("watchdog-mins", 0, "Watchdog timer in minutes (0 to disable)") +} diff --git a/notehub/cmd/helpers.go b/notehub/cmd/helpers.go new file mode 100644 index 0000000..ebdf201 --- /dev/null +++ b/notehub/cmd/helpers.go @@ -0,0 +1,498 @@ +// Copyright 2025 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package cmd + +import ( + "bufio" + "bytes" + "context" + "fmt" + "os" + "sort" + "strings" + + notehub "github.com/blues/notehub-go" +) + +// Type definitions +type Vars map[string]string + +type Metadata struct { + Name string `json:"name,omitempty"` + UID string `json:"uid,omitempty"` + BA string `json:"billing_account_uid,omitempty"` + Vars map[string]string `json:"vars,omitempty"` +} + +type AppMetadata struct { + App Metadata `json:"app,omitempty"` + Fleets []Metadata `json:"fleets,omitempty"` + Routes []Metadata `json:"routes,omitempty"` + Products []Metadata `json:"products,omitempty"` +} + +// GetNotehubClient creates and returns a configured Notehub API client with authentication +func GetNotehubClient() *notehub.APIClient { + cfg := notehub.NewConfiguration() + + // Set the API server if configured + apiHub := GetAPIHub() + if apiHub != "" { + cfg.Host = apiHub + cfg.Scheme = "https" + } + + return notehub.NewAPIClient(cfg) +} + +// GetNotehubContext creates a context with authentication for Notehub API calls +func GetNotehubContext() (context.Context, error) { + // Get the authentication credentials + creds, err := GetHubCredentials() + if err != nil { + return nil, err + } + if creds == nil || creds.Token == "" { + return nil, fmt.Errorf("not authenticated: please use 'notehub auth signin' to sign in") + } + + // Create context with bearer token authentication + ctx := context.WithValue(context.Background(), notehub.ContextAccessToken, creds.Token) + + return ctx, nil +} + +// Load metadata for the app +func appGetMetadata(flagVerbose bool, flagVars bool) (appMetadata AppMetadata, err error) { + // Get project info using SDK + // First we need to determine the project UID from global flags + projectUID := GetProject() + if projectUID == "" { + product := GetProduct() + if product != "" { + projectUID = product + } + } + + if projectUID == "" { + return appMetadata, fmt.Errorf("project or product UID required") + } + + // Initialize SDK client and context + client := GetNotehubClient() + ctx, err := GetNotehubContext() + if err != nil { + return appMetadata, err + } + + // Get project information using SDK + project, _, err := client.ProjectAPI.GetProject(ctx, projectUID).Execute() + if err != nil { + return appMetadata, fmt.Errorf("failed to get project: %w", err) + } + + // App info - fields are direct values, not pointers + appMetadata.App.UID = project.Uid + appMetadata.App.Name = project.Label + // Note: BillingAccountUid is not in the Project model + + // Get fleets using SDK + fleetsResp, _, err := client.ProjectAPI.GetFleets(ctx, appMetadata.App.UID).Execute() + if err == nil && fleetsResp.Fleets != nil { + items := []Metadata{} + for _, fleet := range fleetsResp.Fleets { + i := Metadata{Name: fleet.Label, UID: fleet.Uid} + + if flagVars && fleet.Uid != "" { + varsResp, _, err := client.ProjectAPI.GetFleetEnvironmentVariables(ctx, appMetadata.App.UID, fleet.Uid).Execute() + if err != nil { + return appMetadata, fmt.Errorf("failed to get fleet environment variables: %w", err) + } + i.Vars = varsResp.EnvironmentVariables + } + items = append(items, i) + } + appMetadata.Fleets = items + } + + // Get routes using SDK + routes, _, err := client.RouteAPI.GetRoutes(ctx, appMetadata.App.UID).Execute() + if err == nil && routes != nil { + items := []Metadata{} + for _, route := range routes { + routeUID := "" + routeLabel := "" + if route.Uid != nil { + routeUID = *route.Uid + } + if route.Label != nil { + routeLabel = *route.Label + } + i := Metadata{Name: routeLabel, UID: routeUID} + items = append(items, i) + } + appMetadata.Routes = items + } + + // Get products using SDK + productsResp, _, err := client.ProjectAPI.GetProducts(ctx, appMetadata.App.UID).Execute() + if err == nil && productsResp.Products != nil { + items := []Metadata{} + for _, product := range productsResp.Products { + i := Metadata{Name: product.Label, UID: product.Uid} + items = append(items, i) + } + appMetadata.Products = items + } + + return +} + +// Get a device list given a scope +func appGetScope(scope string, flagVerbose bool) (appMetadata AppMetadata, scopeDevices []string, scopeFleets []string, err error) { + // Process special scopes + switch scope { + case "devices": + scope = "@" + case "fleets": + scope = "-" + } + + // Get the metadata + appMetadata, err = appGetMetadata(flagVerbose, false) + if err != nil { + return + } + + // On the command line we allow comma-separated lists + if strings.Contains(scope, ",") { + scopeList := strings.Split(scope, ",") + for _, scope := range scopeList { + err = addScope(scope, &appMetadata, &scopeDevices, &scopeFleets, flagVerbose) + if err != nil { + return + } + } + } else { + err = addScope(scope, &appMetadata, &scopeDevices, &scopeFleets, flagVerbose) + if err != nil { + return + } + } + + // Remove duplicates + scopeDevices = sortAndRemoveDuplicates(scopeDevices) + scopeFleets = sortAndRemoveDuplicates(scopeFleets) + + return +} + +// ResolveScopeWithValidation is a convenience wrapper around appGetScope that: +// 1. Automatically uses GetVerbose() for the verbose flag +// 2. Validates that at least one device or fleet was found +// 3. Returns a more user-friendly error message +// +// This reduces boilerplate in commands that use scope resolution. +func ResolveScopeWithValidation(scope string) (appMetadata AppMetadata, scopeDevices []string, scopeFleets []string, err error) { + verbose := GetVerbose() + appMetadata, scopeDevices, scopeFleets, err = appGetScope(scope, verbose) + if err != nil { + return + } + + if len(scopeDevices) == 0 && len(scopeFleets) == 0 { + err = fmt.Errorf("no devices or fleets found within the specified scope") + return + } + + return +} + +// Recursively add scope +func addScope(scope string, appMetadata *AppMetadata, scopeDevices *[]string, scopeFleets *[]string, flagVerbose bool) (err error) { + if strings.HasPrefix(scope, "dev:") { + *scopeDevices = append(*scopeDevices, scope) + return + } + + if strings.HasPrefix(scope, "imei:") || strings.HasPrefix(scope, "burn:") { + *scopeDevices = append(*scopeDevices, scope) + return + } + + if strings.HasPrefix(scope, "fleet:") { + *scopeFleets = append(*scopeFleets, scope) + return + } + + // See if this is a fleet name + if !strings.HasPrefix(scope, "@") { + found := false + for _, fleet := range (*appMetadata).Fleets { + if fleetMatchesScope(fleet.Name, scope) { + *scopeFleets = append(*scopeFleets, fleet.UID) + found = true + } + } + if !found { + return fmt.Errorf("'%s' does not appear to be a device, fleet, @fleet indirection, or @file.ext indirection", scope) + } + return + } + + // Process indirection + indirectScope := strings.TrimPrefix(scope, "@") + foundFleet := false + lookingFor := strings.TrimSpace(indirectScope) + + // Get SDK client for device queries + client := GetNotehubClient() + ctx, err := GetNotehubContext() + if err != nil { + return + } + + // Looking for "all devices" or a named fleet + if indirectScope == "" { + // All devices - use SDK + pageSize := int32(500) + pageNum := int32(0) + for { + pageNum++ + + devicesResp, _, err := client.DeviceAPI.GetDevices(ctx, appMetadata.App.UID). + PageSize(pageSize). + PageNum(pageNum). + Execute() + if err != nil { + return err + } + + for _, device := range devicesResp.Devices { + err = addScope(device.Uid, appMetadata, scopeDevices, scopeFleets, flagVerbose) + if err != nil { + return err + } + } + + if !devicesResp.HasMore { + break + } + } + return + } else { + // Fleet - use SDK + for _, fleet := range (*appMetadata).Fleets { + if lookingFor == fleet.UID || fleetMatchesScope(fleet.Name, lookingFor) { + foundFleet = true + + pageSize := int32(100) + pageNum := int32(0) + for { + pageNum++ + + devicesResp, _, err := client.DeviceAPI.GetFleetDevices(ctx, appMetadata.App.UID, fleet.UID). + PageSize(pageSize). + PageNum(pageNum). + Execute() + if err != nil { + return err + } + + for _, device := range devicesResp.Devices { + err = addScope(device.Uid, appMetadata, scopeDevices, scopeFleets, flagVerbose) + if err != nil { + return err + } + } + + if !devicesResp.HasMore { + break + } + } + } + } + if foundFleet { + return + } + } + + // Process a file indirection + var contents []byte + contents, err = os.ReadFile(indirectScope) + if err != nil { + return fmt.Errorf("%s: %s", indirectScope, err) + } + + scanner := bufio.NewScanner(bytes.NewReader(contents)) + scanner.Split(bufio.ScanLines) + + for scanner.Scan() { + line := scanner.Text() + if trimmedLine := strings.TrimSpace(line); trimmedLine != "" { + err = addScope(trimmedLine, appMetadata, scopeDevices, scopeFleets, flagVerbose) + if err != nil { + return err + } + } + } + + err = scanner.Err() + return +} + +// Sort and remove duplicates +func sortAndRemoveDuplicates(strings []string) []string { + sort.Strings(strings) + + unique := make(map[string]struct{}) + var result []string + + for _, v := range strings { + if _, exists := unique[v]; !exists { + unique[v] = struct{}{} + result = append(result, v) + } + } + + return result +} + +// See if a fleet name matches a scope name +func fleetMatchesScope(fleetName string, scope string) bool { + normalizedScope := strings.ToLower(scope) + scopeWildcard := false + if strings.HasSuffix(normalizedScope, "*") { + normalizedScope = strings.TrimSuffix(normalizedScope, "*") + scopeWildcard = true + } + normalizedName := strings.ToLower(fleetName) + match := scope == "-" || normalizedName == normalizedScope + if scopeWildcard { + if strings.HasPrefix(normalizedName, normalizedScope) { + match = true + } + } + return match +} + +// Load env vars from devices +func varsGetFromDevices(appMetadata AppMetadata, uids []string, flagVerbose bool) (vars map[string]Vars, err error) { + vars = map[string]Vars{} + + client := GetNotehubClient() + ctx, err := GetNotehubContext() + if err != nil { + return + } + + for _, deviceUID := range uids { + varsResp, _, err := client.DeviceAPI.GetDeviceEnvironmentVariables(ctx, appMetadata.App.UID, deviceUID).Execute() + if err != nil { + return vars, err + } + vars[deviceUID] = varsResp.EnvironmentVariables + } + + return +} + +// Load env vars from fleets +func varsGetFromFleets(appMetadata AppMetadata, uids []string, flagVerbose bool) (vars map[string]Vars, err error) { + vars = map[string]Vars{} + + client := GetNotehubClient() + ctx, err := GetNotehubContext() + if err != nil { + return + } + + for _, fleetUID := range uids { + varsResp, _, err := client.ProjectAPI.GetFleetEnvironmentVariables(ctx, appMetadata.App.UID, fleetUID).Execute() + if err != nil { + return vars, err + } + vars[fleetUID] = varsResp.EnvironmentVariables + } + + return +} + +// Set env vars for devices +func varsSetFromDevices(appMetadata AppMetadata, uids []string, template Vars, flagVerbose bool) (vars map[string]Vars, err error) { + vars = map[string]Vars{} + + client := GetNotehubClient() + ctx, err := GetNotehubContext() + if err != nil { + return + } + + for _, deviceUID := range uids { + envVars := notehub.NewEnvironmentVariables(template) + + varsResp, _, err := client.DeviceAPI.SetDeviceEnvironmentVariables(ctx, appMetadata.App.UID, deviceUID). + EnvironmentVariables(*envVars). + Execute() + if err != nil { + return vars, err + } + + vars[deviceUID] = varsResp.EnvironmentVariables + } + + return +} + +// Set env vars for fleets +func varsSetFromFleets(appMetadata AppMetadata, uids []string, template Vars, flagVerbose bool) (vars map[string]Vars, err error) { + vars = map[string]Vars{} + + client := GetNotehubClient() + ctx, err := GetNotehubContext() + if err != nil { + return + } + + for _, fleetUID := range uids { + envVars := notehub.NewEnvironmentVariables(template) + + varsResp, _, err := client.ProjectAPI.SetFleetEnvironmentVariables(ctx, appMetadata.App.UID, fleetUID). + EnvironmentVariables(*envVars). + Execute() + if err != nil { + return vars, err + } + + vars[fleetUID] = varsResp.EnvironmentVariables + } + + return +} + +// Provision devices +func varsProvisionDevices(appMetadata AppMetadata, uids []string, productUID string, deviceSN string, flagVerbose bool) (err error) { + client := GetNotehubClient() + ctx, err := GetNotehubContext() + if err != nil { + return + } + + for _, deviceUID := range uids { + provReq := notehub.NewProvisionDeviceRequest(productUID) + if deviceSN != "" { + provReq.SetDeviceSn(deviceSN) + } + + _, _, err = client.DeviceAPI.ProvisionDevice(ctx, appMetadata.App.UID, deviceUID). + ProvisionDeviceRequest(*provReq). + Execute() + if err != nil { + return + } + } + + return +} diff --git a/notehub/cmd/openapi.yaml b/notehub/cmd/openapi.yaml new file mode 100644 index 0000000..e4dd14e --- /dev/null +++ b/notehub/cmd/openapi.yaml @@ -0,0 +1,13004 @@ +openapi: 3.0.3 +info: + contact: + email: engineering@blues.io + name: Blues Engineering + url: https://dev.blues.io/support/ + description: | + The OpenAPI definition for the Notehub.io API. + title: Notehub API + version: 1.2.0 +externalDocs: + description: Find out more about Blues + url: https://blues.io +servers: +- description: Production server + url: https://api.notefile.net +tags: +- description: Authorization operations + name: authorization +- description: Project operations + name: project +- description: Event retrieval operations + name: event +- description: Device operations + name: device +- description: Billing Account operations + name: billing_account +- description: Management of monitors + name: monitors +- description: Route operations + name: route +- description: Webhook APIs for non-notecard event ingestion + name: webhook +- description: APIs for events and sessions for external devices + name: external devices +- description: "Project Usage information related to events, route logs, sessions,\ + \ and data usage" + name: usage +paths: + /auth/login: + post: + description: Gets an API key from username and password + operationId: Login + requestBody: + content: + application/json: + example: + username: name@example.com + password: test-password + schema: + $ref: '#/components/schemas/Login_request' + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Login_200_response' + description: Successful operation + "400": + description: Bad Request + "500": + description: Internal Server Error + tags: + - authorization + /oauth2/token: + post: + description: | + Exchanges client credentials for an access token. Parameters must be sent as application/x-www-form-urlencoded. + operationId: OAuth2ClientCredentials + requestBody: + content: + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/OAuth2ClientCredentials_request' + required: true + responses: + "200": + content: + application/json: + example: + access_token: 7VEVIiNVr_Q_eqjWpddz18qmYVWVgKHtlYsuoYzS7v0.3BUMglZCKBF1Swz0WLQQ9-DgSBtkroKbpXmF1sJ-NE0 + expires_in: 1800 + scope: openid + token_type: bearer + schema: + $ref: '#/components/schemas/OAuth2TokenResponse' + description: Successful token response + "400": + content: + application/json: + example: + error: invalid_request + error_description: "Missing parameter: client_id" + schema: + $ref: '#/components/schemas/OAuth2Error' + description: Invalid request (missing or malformed parameters) + "401": + content: + application/json: + example: + error: invalid_client + error_description: Client authentication failed + schema: + $ref: '#/components/schemas/OAuth2Error' + description: Invalid client authentication + "403": + content: + application/json: + example: + error: invalid_scope + error_description: Requested scope is invalid + schema: + $ref: '#/components/schemas/OAuth2Error' + description: Unauthorized scope + summary: Issue an OAuth 2.0 access token (Client Credentials) + tags: + - authorization + /v1/projects/{projectOrProductUID}/routes: + get: + description: Get all Routes within a Project + operationId: GetRoutes + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + example: + - uid: route:8d65a087d5d290ce5bdf03aeff2becc0 + label: success route + type: http + modified: 2020-03-09T17:58:37Z + disabled: false + - uid: route:a9eaad31d5cee8d01a42762f71fb777a + label: failing route + type: http + modified: 2020-03-09T17:59:15Z + disabled: false + - uid: route:02ddc0e6e236c2a7e482da62047229ad + label: disabled route + type: http + modified: 2020-03-09T17:59:44Z + disabled: true + - uid: route:0ac565deb7b478a250bb82348b9cfdd4 + label: Proxy Route + type: proxy + modified: 2020-03-09T17:58:36Z + disabled: false + - uid: route:fb1b9e0aba1bf030311ba2c3c1e3efd7 + label: Myjsonlive Webtest + type: proxy + modified: 2020-03-09T17:58:35Z + disabled: false + - uid: route:7804818f84a3be6193e14d804fe7fca7 + label: Myjsonlive Echo + type: proxy + modified: 2020-03-09T17:58:34Z + disabled: false + schema: + items: + $ref: '#/components/schemas/NotehubRouteSummary' + minItems: 0 + type: array + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - route + post: + description: Create Route within a Project + operationId: CreateRoute + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + requestBody: + content: + application/json: + examples: + http: + summary: HTTP route + value: + label: Route Label + http: + fleets: + - fleet:1042ddc5-3b2c-4cec-b1fb-d3040538094d + filter: {} + transform: {} + throttle_ms: 100 + url: https://example.com/ingest + http_headers: + X-My-Header: value + disable_http_headers: false + timeout: 5000 + schema: + $ref: '#/components/schemas/NotehubRoute' + description: Route to be created + required: true + responses: + "201": + content: + application/json: + example: + uid: route:8d65a087d5d290ce5bdf03aeff2becc0 + label: Route Label + type: http + modified: 2020-03-09T17:59:44Z + disabled: "false" + http: + fleets: + - fleet:1042ddc5-3b2c-4cec-b1fb-d3040538094d + filter: + type: "" + system_notefiles: false + transform: {} + throttle_ms: 100 + url: http://route.url + http_headers: null + disable_http_headers: false + timeout: 0 + schema: + $ref: '#/components/schemas/NotehubRoute' + description: Created + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - route + /v1/projects/{projectOrProductUID}/routes/{routeUID}: + delete: + description: Delete single route within a project + operationId: DeleteRoute + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: route:cbd20093cba58392c9f9bbdd0cdeb1a0 + explode: false + in: path + name: routeUID + required: true + schema: + type: string + style: simple + responses: + "204": + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - route + get: + description: Get single route within a project + operationId: GetRoute + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: route:cbd20093cba58392c9f9bbdd0cdeb1a0 + explode: false + in: path + name: routeUID + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + example: + uid: route:8d65a087d5d290ce5bdf03aeff2becc0 + label: Route Label + type: http + modified: 2020-03-09T17:59:44Z + disabled: "false" + http: + fleets: + - fleet:1042ddc5-3b2c-4cec-b1fb-d3040538094d + filter: + type: "" + system_notefiles: false + transform: {} + throttle_ms: 100 + url: http://route.url + http_headers: null + disable_http_headers: false + timeout: 0 + schema: + $ref: '#/components/schemas/NotehubRoute' + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - route + put: + description: Update route by UID + operationId: UpdateRoute + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: route:cbd20093cba58392c9f9bbdd0cdeb1a0 + explode: false + in: path + name: routeUID + required: true + schema: + type: string + style: simple + requestBody: + content: + application/json: + example: | + { + "http" { + "filter": { + "type": "include", + "system_notefiles": true, + "files": ["somefile.qo"], + }, + "throttle_ms": 50, + "url": "http://new-route.url", + }, + } + schema: + $ref: '#/components/schemas/NotehubRoute' + description: Route settings to be updated + required: true + responses: + "200": + content: + application/json: + example: | + { + "uid": "route:8d65a087d5d290ce5bdf03aeff2becc0", + "label": "Route Label", + "type": "http", + "modified": "2020-03-09T17:59:44Z", + "disabled": "false", + "http": + { + "fleets": ["fleet:1042ddc5-3b2c-4cec-b1fb-d3040538094d"], + "filter": { + "type": "include", + "system_notefiles": true, + "files": ["somefile.qo"], + }, + "transform": {}, + "throttle_ms": 50, + "url": "http://new-route.url", + "http_headers": null, + "disable_http_headers": false, + "timeout": 0 + } + schema: + $ref: '#/components/schemas/NotehubRoute' + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - route + /v1/projects/{projectOrProductUID}/routes/{routeUID}/route-logs: + get: + description: Get Route Logs by Route UID + operationId: GetRouteLogsByRoute + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: route:cbd20093cba58392c9f9bbdd0cdeb1a0 + explode: false + in: path + name: routeUID + required: true + schema: + type: string + style: simple + - explode: true + in: query + name: pageSize + required: false + schema: + default: 50 + maximum: 10000 + minimum: 1 + type: integer + style: form + - explode: true + in: query + name: pageNum + required: false + schema: + default: 1 + minimum: 1 + type: integer + style: form + - description: A Device UID. + explode: true + in: query + name: deviceUID + required: false + schema: + items: + type: string + type: array + style: form + - explode: true + in: query + name: sortBy + required: false + schema: + default: date + enum: + - date + - event + type: string + style: form + - explode: true + in: query + name: sortOrder + required: false + schema: + default: desc + enum: + - asc + - desc + type: string + style: form + - description: "Start date for filtering results, specified as a Unix timestamp" + example: 1628631763 + explode: true + in: query + name: startDate + required: false + schema: + minimum: 0 + type: integer + style: form + - description: "End date for filtering results, specified as a Unix timestamp" + example: 1657894210 + explode: true + in: query + name: endDate + required: false + schema: + minimum: 0 + type: integer + style: form + - explode: true + in: query + name: systemFilesOnly + required: false + schema: + type: boolean + style: form + - explode: true + in: query + name: mostRecentOnly + required: false + schema: + type: boolean + style: form + - example: "_health.qo, data.qo" + explode: true + in: query + name: files + required: false + schema: + type: string + style: form + responses: + "200": + content: + application/json: + schema: + items: + $ref: '#/components/schemas/RouteLog' + type: array + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - route + /v1/products/{productUID}/project: + get: + description: Get a Project by ProductUID + operationId: GetProjectByProduct + parameters: + - example: com.blues.airnote + explode: false + in: path + name: productUID + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Project' + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - project + /v1/billing-accounts: + get: + description: Get Billing Accounts accessible by the api_key + operationId: GetBillingAccounts + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/GetBillingAccounts_200_response' + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - billing_account + /v1/projects: + get: + description: Get Projects accessible by the api_key + operationId: GetProjects + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/GetProjects_200_response' + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - project + post: + description: Create a Project + operationId: CreateProject + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreateProject_request' + description: Project to be created + required: true + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/Project' + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - project + /v1/projects/{projectOrProductUID}/clone: + post: + description: Clone a Project + operationId: CloneProject + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CloneProject_request' + description: Project to be cloned + required: true + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/Project' + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - project + /v1/projects/{projectOrProductUID}: + delete: + description: Delete a Project by ProjectUID + operationId: DeleteProject + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + responses: + "200": + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - project + get: + description: Get a Project by ProjectUID + operationId: GetProject + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Project' + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - project + /v1/projects/{projectOrProductUID}/products: + get: + description: Get Products within a Project + operationId: GetProducts + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/GetProducts_200_response' + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - project + post: + description: Create Product within a Project + operationId: CreateProduct + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreateProduct_request' + description: Product to be created + required: true + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/Product' + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - project + /v1/projects/{projectOrProductUID}/products/{productUID}: + delete: + description: Delete a product + operationId: DeleteProduct + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: com.blues.bridge:sensors + explode: false + in: path + name: productUID + required: true + schema: + type: string + style: simple + responses: + "204": + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - project + /v1/projects/{projectOrProductUID}/fleets: + get: + description: Get Project Fleets + operationId: GetFleets + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/GetFleets_200_response' + description: The response body from a fleets endpoint. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - project + post: + description: Create Fleet + operationId: CreateFleet + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreateFleet_request' + description: Fleet to be added + required: true + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/Fleet' + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - project + /v1/projects/{projectOrProductUID}/fleets/{fleetUID}: + delete: + description: Delete Fleet + operationId: DeleteFleet + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - explode: false + in: path + name: fleetUID + required: true + schema: + type: string + style: simple + responses: + "204": + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - project + get: + description: Get Fleet + operationId: GetFleet + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - explode: false + in: path + name: fleetUID + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Fleet' + description: Successful operation + security: + - personalAccessToken: [] + tags: + - project + put: + description: Update Fleet + operationId: UpdateFleet + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - explode: false + in: path + name: fleetUID + required: true + schema: + type: string + style: simple + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateFleet_request' + description: Fleet details to update + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Fleet' + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - project + /v1/projects/{projectOrProductUID}/members: + get: + description: Get Project Members + operationId: GetProjectMembers + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/GetProjectMembers_200_response' + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - project + /v1/projects/{projectOrProductUID}/devices: + get: + description: Get Devices of a Project + operationId: GetDevices + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - explode: true + in: query + name: pageSize + required: false + schema: + default: 50 + maximum: 10000 + minimum: 1 + type: integer + style: form + - explode: true + in: query + name: pageNum + required: false + schema: + default: 1 + minimum: 1 + type: integer + style: form + - description: A Device UID. + explode: true + in: query + name: deviceUID + required: false + schema: + items: + type: string + type: array + style: form + - description: Tag filter + explode: true + in: query + name: tag + required: false + schema: + items: + type: string + type: array + style: form + - description: Serial number filter + explode: true + in: query + name: serialNumber + required: false + schema: + items: + type: string + type: array + style: form + - explode: true + in: query + name: fleetUID + required: false + schema: + items: + type: string + type: array + style: form + - description: Firmware version filter + explode: true + in: query + name: notecardFirmware + required: false + schema: + items: + type: string + type: array + style: form + - description: Location filter + explode: true + in: query + name: location + required: false + schema: + items: + type: string + type: array + style: form + - description: Host firmware filter + explode: true + in: query + name: hostFirmware + required: false + schema: + items: + type: string + type: array + style: form + - explode: true + in: query + name: productUID + required: false + schema: + items: + type: string + type: array + style: form + - description: SKU filter + explode: true + in: query + name: sku + required: false + schema: + items: + type: string + type: array + style: form + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/GetDevices_200_response' + description: List of Devices + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - device + /v1/projects/{projectOrProductUID}/devices/{deviceUID}/provision: + post: + description: Provision Device for a Project + operationId: ProvisionDevice + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ProvisionDevice_request' + description: Provision a device to a specific ProductUID + required: true + responses: + "200": + content: + application/json: + schema: + default: {} + type: object + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - device + /v1/projects/{projectOrProductUID}/fleets/{fleetUID}/devices: + get: + description: Get Devices of a Fleet within a Project + operationId: GetFleetDevices + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - explode: false + in: path + name: fleetUID + required: true + schema: + type: string + style: simple + - explode: true + in: query + name: pageSize + required: false + schema: + default: 50 + maximum: 10000 + minimum: 1 + type: integer + style: form + - explode: true + in: query + name: pageNum + required: false + schema: + default: 1 + minimum: 1 + type: integer + style: form + - description: A Device UID. + explode: true + in: query + name: deviceUID + required: false + schema: + items: + type: string + type: array + style: form + - description: Tag filter + explode: true + in: query + name: tag + required: false + schema: + items: + type: string + type: array + style: form + - description: Serial number filter + explode: true + in: query + name: serialNumber + required: false + schema: + items: + type: string + type: array + style: form + - description: Firmware version filter + explode: true + in: query + name: notecardFirmware + required: false + schema: + items: + type: string + type: array + style: form + - description: Location filter + explode: true + in: query + name: location + required: false + schema: + items: + type: string + type: array + style: form + - description: Host firmware filter + explode: true + in: query + name: hostFirmware + required: false + schema: + items: + type: string + type: array + style: form + - explode: true + in: query + name: productUID + required: false + schema: + items: + type: string + type: array + style: form + - description: SKU filter + explode: true + in: query + name: sku + required: false + schema: + items: + type: string + type: array + style: form + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/GetDevices_200_response' + description: List of Devices + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - device + /v1/projects/{projectOrProductUID}/devices/public-keys: + get: + description: Get Device Public Keys of a Project + operationId: GetDevicePublicKeys + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - explode: true + in: query + name: pageSize + required: false + schema: + default: 50 + maximum: 10000 + minimum: 1 + type: integer + style: form + - explode: true + in: query + name: pageNum + required: false + schema: + default: 1 + minimum: 1 + type: integer + style: form + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/GetDevicePublicKeys_200_response' + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - device + /v1/projects/{projectOrProductUID}/devices/{deviceUID}: + delete: + description: Delete Device + operationId: DeleteDevice + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + - explode: true + in: query + name: purge + required: true + schema: + default: false + type: boolean + style: form + responses: + "204": + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - device + get: + description: Get Device + operationId: GetDevice + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Device' + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - device + /v1/projects/{projectOrProductUID}/devices/{deviceUID}/plans: + get: + description: "Get Data Plans associated with the device, this include the primary\ + \ sim, any external sim, as well as any satellite connections." + operationId: GetDevicePlans + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/GetDevicePlans_200_response' + description: Response body for /plans + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - device + /v1/projects/{projectOrProductUID}/devices/{deviceUID}/fleets: + delete: + description: Remove Device from Fleets + operationId: DeleteDeviceFromFleets + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DeleteDeviceFromFleets_request' + description: | + The fleets to remove from the device. Note that the endpoint takes an array of fleetUIDs, to facilitate multi-fleet devices. Multi-fleet is not yet enabled on all SaaS plans - unless it is supported by the SaaS plan of the project, passing more than a single fleetUID in the array is an error. + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/GetFleets_200_response' + description: The response body from a fleets endpoint. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - project + get: + description: Get Device Fleets + operationId: GetDeviceFleets + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/GetFleets_200_response' + description: The response body from a fleets endpoint. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - project + put: + description: Add Device to Fleets + operationId: AddDeviceToFleets + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/AddDeviceToFleets_request' + description: | + The fleets to add to the device. Note that the endpoint takes an array of fleetUIDs, to facilitate multi-fleet devices. Multi-fleet is not yet enabled on all SaaS plans - unless it is supported by the SaaS plan of the project, passing more than a single fleetUID in the array is an error. + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/GetFleets_200_response' + description: The response body from a fleets endpoint. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - project + /v1/projects/{projectOrProductUID}/devices/{deviceUID}/public-key: + get: + description: Get Device Public Key + operationId: GetDevicePublicKey + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/GetDevicePublicKey_200_response' + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - device + /v1/projects/{projectOrProductUID}/devices/{deviceUID}/latest: + get: + description: Get Device Latest Events + operationId: GetDeviceLatestEvents + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + example: + latest_events: + - event: 81bd2bf1-0399-4978-bc46-8f779b4af350 + session: ed18884b-f2a6-419f-b856-d28dc8f0892b + tls: true + device: dev:864475040523995 + product: product:com.blues.app:myapp + app: app:2e49f10a-76a9-4e2d-8b18-cef0b8b46446 + received: 1.669667707564694E9 + req: session.begin + when: 1669667707 + file: _session.qo + body: + why: sensors.qo requested sync (sensors.qo) (TLS) + tower_when: 1669667691 + tower_lat: 43.769062500000004 + tower_lon: -83.657359375 + tower_country: US + tower_location: Waverly MI + tower_timezone: America/Detroit + tower_id: "310,410,20483,184692495" + - event: 916d4c81-06ae-4263-9b55-7a3a0f73cb5a + session: 28cdc39f-9f62-4789-b0a3-2f35f9448ced + device: dev:864475040523995 + sn: tj-1 + product: product:com.blues.app:myapp + app: app:2e49f10a-76a9-4e2d-8b18-cef0b8b46446 + received: 1.669667713221659E9 + req: note.add + when: 1669667689 + file: data.qo + body: + humid: 56.23 + temp: 35.5 + tower_when: 1669667677 + tower_lat: 43.769062500000004 + tower_lon: -83.657359375 + tower_country: US + tower_location: Waverly MI + tower_timezone: America/Detroit + tower_id: "310,410,20483,184692495" + - event: e98c2c3b-edbe-4fe7-af57-2196cc843eb7 + session: 7211392c-6895-43f8-9256-790655348be5 + device: dev:864475040523995 + product: product:com.blues.app:myapp + app: app:2e49f10a-76a9-4e2d-8b18-cef0b8b46446 + received: 1.66966771185316E9 + req: note.add + when: 1669667695 + file: sensors.qo + body: + humidity: 69.88647200683693 + pressure: 993.6294496104914 + temp: 21.273027181770885 + tower_when: 1669667689 + tower_lat: 43.747037500000005 + tower_lon: -83.665859375 + tower_country: US + tower_location: Waverly MI + tower_timezone: America/Detroit + tower_id: "310,410,20483,184692496" + schema: + $ref: '#/components/schemas/GetDeviceLatestEvents_200_response' + description: The response body for a Latest Events request. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - device + /v1/projects/{projectOrProductUID}/devices/{deviceUID}/health-log: + get: + description: Get Device Health Log + operationId: GetDeviceHealthLog + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + - description: "Start date for filtering results, specified as a Unix timestamp" + example: 1628631763 + explode: true + in: query + name: startDate + required: false + schema: + minimum: 0 + type: integer + style: form + - description: "End date for filtering results, specified as a Unix timestamp" + example: 1657894210 + explode: true + in: query + name: endDate + required: false + schema: + minimum: 0 + type: integer + style: form + - description: Return only specified log types + explode: true + in: query + name: log_type + required: false + schema: + items: + enum: + - boot + - dfu_completed + type: string + type: array + style: form + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/GetDeviceHealthLog_200_response' + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - device + /v1/projects/{projectOrProductUID}/devices/{deviceUID}/enable: + post: + description: Enable Device + operationId: EnableDevice + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + responses: + "200": + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - device + /v1/projects/{projectOrProductUID}/devices/{deviceUID}/disable: + post: + description: Disable Device + operationId: DisableDevice + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + responses: + "200": + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - device + /v1/projects/{projectOrProductUID}/devices/{deviceUID}/enable-connectivity-assurance: + post: + description: Enable Connectivity Assurance + operationId: EnableDeviceConnectivityAssurance + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + responses: + "200": + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - device + /v1/projects/{projectOrProductUID}/devices/{deviceUID}/disable-connectivity-assurance: + post: + description: Disable Connectivity Assurance + operationId: DisableDeviceConnectivityAssurance + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + responses: + "200": + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - device + /v1/projects/{projectOrProductUID}/devices/{deviceUID}/sessions: + get: + description: Get Device Sessions + operationId: GetDeviceSessions + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + - explode: true + in: query + name: pageSize + required: false + schema: + default: 50 + maximum: 10000 + minimum: 1 + type: integer + style: form + - explode: true + in: query + name: pageNum + required: false + schema: + default: 1 + minimum: 1 + type: integer + style: form + - description: "Start date for filtering results, specified as a Unix timestamp" + example: 1628631763 + explode: true + in: query + name: startDate + required: false + schema: + minimum: 0 + type: integer + style: form + - description: "End date for filtering results, specified as a Unix timestamp" + example: 1657894210 + explode: true + in: query + name: endDate + required: false + schema: + minimum: 0 + type: integer + style: form + responses: + "200": + content: + application/json: + example: + sessions: + - session: d76689be-37cd-423c-b695-7e0c19a2a264 + device: dev:000000000000000 + product: product:com.blues.demo:project + fleets: + - fleet:46be9834-5te6-42c1-0000-b5ea05e248d7 + cell: "310,410,17169,77315594" + rssi: -61 + sinr: 183 + rsrp: -91 + rsrq: -13 + bars: 2 + rat: lte + bearer: LTE FDD + ip: 10.68.56.193 + iccid: "89011704278500000000" + apn: a-notehub.com.attz + tower: + time: 1667250835 + "n": Shorewood Hills WI + c: US + lat: 43.0742625 + lon: -89.44239062499999 + zone: America/Chicago + mcc: 310 + mnc: 410 + lac: 17169 + cid: 77315594 + l: 86MG3HF5+P25 + count: 7 + towers: 1 + tri: {} + when: 1667251044 + voltage: 4.174 + temp: 24.437 + continuous: true + tls: true + work: 1667251046 + events: 14 + moved: 1667250807 + orientation: face-up + trigger: first sync; continuous connection mode + hp_secs_total: 7659 + hp_secs_data: 7659 + hp_cycles_total: 3 + hp_cycles_data: 3 + period: + since: 1667250832 + duration: 215 + bytes_rcvd: 2501 + bytes_sent: 4138 + sessions_tls: 1 + notes_sent: 12 + has_more: true + schema: + $ref: '#/components/schemas/GetDeviceSessions_200_response' + description: The response body for a session request. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - device + /v1/projects/{projectOrProductUID}/events: + get: + description: Get Events of a Project + operationId: GetEvents + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - explode: true + in: query + name: pageSize + required: false + schema: + default: 50 + maximum: 10000 + minimum: 1 + type: integer + style: form + - explode: true + in: query + name: pageNum + required: false + schema: + default: 1 + minimum: 1 + type: integer + style: form + - description: A Device UID. + explode: true + in: query + name: deviceUID + required: false + schema: + items: + type: string + type: array + style: form + - explode: true + in: query + name: sortBy + required: false + schema: + default: captured + enum: + - best_id + - device_serial + - device_uid + - captured + - modified + - device_location + - tower_location + - triangulated_location + - best_location + type: string + style: form + - explode: true + in: query + name: sortOrder + required: false + schema: + default: asc + enum: + - asc + - desc + type: string + style: form + - description: "Start date for filtering results, specified as a Unix timestamp" + example: 1628631763 + explode: true + in: query + name: startDate + required: false + schema: + minimum: 0 + type: integer + style: form + - description: "End date for filtering results, specified as a Unix timestamp" + example: 1657894210 + explode: true + in: query + name: endDate + required: false + schema: + minimum: 0 + type: integer + style: form + - description: "Which date to filter on, either 'captured' or 'uploaded'. This\ + \ will apply to the startDate and endDate parameters" + example: uploaded + explode: true + in: query + name: dateType + required: false + schema: + default: captured + enum: + - captured + - uploaded + type: string + style: form + - explode: true + in: query + name: systemFilesOnly + required: false + schema: + type: boolean + style: form + - example: "_health.qo, data.qo" + explode: true + in: query + name: files + required: false + schema: + type: string + style: form + - description: Response format (JSON or CSV) + explode: true + in: query + name: format + required: false + schema: + default: json + enum: + - json + - csv + type: string + style: form + - description: Filter by Serial Number + explode: true + in: query + name: serialNumber + required: false + schema: + items: + type: string + type: array + style: form + - description: Filter by Fleet UID + explode: true + in: query + name: fleetUID + required: false + schema: + items: + type: string + type: array + style: form + - description: Filter by Session UID + explode: true + in: query + name: sessionUID + required: false + schema: + items: + type: string + type: array + style: form + - description: Filter by Event UID + explode: true + in: query + name: eventUID + required: false + schema: + items: + type: string + type: array + style: form + - description: "Comma-separated list of fields to select from JSON payload (e.g.,\ + \ \"field1,field2.subfield,field3\"), this will reflect the columns in the\ + \ CSV output." + explode: true + in: query + name: selectFields + required: false + schema: + type: string + style: form + - deprecated: true + description: Deprecated. + explode: true + in: query + name: deviceUIDs + required: false + schema: + items: + type: string + type: array + style: form + - deprecated: true + description: Deprecated. + explode: true + in: query + name: since + required: false + schema: + type: string + style: form + responses: + "200": + content: + application/json: + example: + events: + - event: dfa3747d-688b-4250-935b-5dd60354313c + session: b623132c-6afb-4740-bc39-e3634e38f064 + best_id: My Device + device: dev:5c0272311928 + sn: My Device + product: product:com.blues.project.demo + app: app:218f6217-9f78-432e-9fe0-02ca8b5a216c + received: 1.656011227006928E9 + req: note.add + when: 1656010061 + file: air.qo + updates: 1 + body: + humidity: 40.375 + pressure: 97705.66 + temperature: 24.0625 + voltage: 2.598 + best_location_type: triangulated + best_location_when: 1652709545 + best_lat: 34.82476372 + best_lon: -83.32261614 + best_location: Atlanta GA + best_country: US + best_timezone: America/New_York + tower_id: "0,0,0,0" + tri_when: 1652709545 + tri_lat: 34.82475372 + tri_lon: -83.32261614 + tri_location: Atlanta GA + tri_country: US + tri_timezone: America/New_York + tri_points: 6 + has_more: true + schema: + $ref: '#/components/schemas/GetEvents_200_response' + text/csv: + example: !!binary |- + ZXZlbnRVSUQsZGV2aWNlVUlELHdoZW4sYmVzdF9sb2NhdGlvbl90eXBlLGJlc3RfbGF0LGJlc3Rf + bG9uLGJvZHkudGVtcGVyYXR1cmUsYm9keS5odW1pZGl0eQplMTIzNDU2LTc4OTAtYWJjZC1lZjAx + LTIzNDU2Nzg5MGFiYyxkZXY6MDAwMDAwMDAwMDAwMDAxLDE2MjUwOTc2MDAsZ3BzLDM3Ljc3NDks + LTEyMi40MTk0LDIyLjUsNDUuMgpmMjM0NTY3LTg5MDEtYmNkZS1mZzEyLTM0NTY3ODkwMWJjZCxk + ZXY6MDAwMDAwMDAwMDAwMDAyLDE2MjUwOTc2NjAsdHJpYW5ndWxhdGVkLDQwLjcxMjgsLTc0LjAw + NjAsMjQuMyw0OC43CmczNDU2NzgtOTAxMi1jZGVmLWdoMjMtNDU2Nzg5MDEyY2RlLGRldjowMDAw + MDAwMDAwMDAwMDMsMTYyNTA5NzcyMCx0b3dlciw1MS41MDc0LC0wLjEyNzgsMjAuMSw1Mi45Cg== + schema: + format: binary + type: string + description: The response body from a GET events request. + headers: + X-Has-More: + description: True if there are more events + explode: false + schema: + type: boolean + style: simple + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - event + /v1/projects/{projectOrProductUID}/events-cursor: + get: + description: Get Events of a Project by cursor + operationId: GetEventsByCursor + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - explode: true + in: query + name: limit + required: false + schema: + default: 50 + minimum: 1 + type: integer + style: form + - description: | + A cursor, which can be obtained from the `next_cursor` value from a previous call to this endpoint. The results set returned will include this event as its first result if the given identifier is actually the UID of an event. If this event UID is not found, the parameter is ignored and the results set is the same as if the parameter was not included. + explode: true + in: query + name: cursor + required: false + schema: + type: string + style: form + - explode: true + in: query + name: sortOrder + required: false + schema: + default: asc + enum: + - asc + - desc + type: string + style: form + - explode: true + in: query + name: systemFilesOnly + required: false + schema: + type: boolean + style: form + - example: "_health.qo, data.qo" + explode: true + in: query + name: files + required: false + schema: + type: string + style: form + - explode: true + in: query + name: fleetUID + required: false + schema: + type: string + style: form + - description: A Device UID. + explode: true + in: query + name: deviceUID + required: false + schema: + items: + type: string + type: array + style: form + responses: + "200": + content: + application/json: + example: + events: + - event: dfa3747d-688b-4250-935b-5dd60354313c + session: b623132c-6afb-4740-bc39-e3634e38f064 + best_id: My Device + device: dev:5c0272311928 + sn: My Device + product: product:com.blues.project.demo + app: app:218f6217-9f78-432e-9fe0-02ca8b5a216c + received: 1.656011227006928E9 + req: note.add + when: 1656010061 + file: air.qo + updates: 1 + body: + humidity: 40.375 + pressure: 97705.66 + temperature: 24.0625 + voltage: 2.598 + best_location_type: triangulated + best_location_when: 1652709545 + best_lat: 34.82476372 + best_lon: -83.32261614 + best_location: Atlanta GA + best_country: US + best_timezone: America/New_York + tower_id: "0,0,0,0" + tri_when: 1652709545 + tri_lat: 34.82475372 + tri_lon: -83.32261614 + tri_location: Atlanta GA + tri_country: US + tri_timezone: America/New_York + tri_points: 6 + next_cursor: "" + has_more: false + schema: + $ref: '#/components/schemas/GetEventsByCursor_200_response' + description: The response body from a GET events by cursor request. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - event + /v1/projects/{projectOrProductUID}/events/{eventUID}/route-logs: + get: + description: Get Route Logs by Event UID + operationId: GetRouteLogsByEvent + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: 4506f411-dea6-44a0-9743-1130f57d7747 + explode: false + in: path + name: eventUID + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + items: + $ref: '#/components/schemas/RouteLog' + type: array + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - event + /v1/projects/{projectOrProductUID}/fleets/{fleetUID}/events: + get: + description: Get Events of a Fleet + operationId: GetFleetEvents + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - explode: false + in: path + name: fleetUID + required: true + schema: + type: string + style: simple + - explode: true + in: query + name: pageSize + required: false + schema: + default: 50 + maximum: 10000 + minimum: 1 + type: integer + style: form + - explode: true + in: query + name: pageNum + required: false + schema: + default: 1 + minimum: 1 + type: integer + style: form + - description: A Device UID. + explode: true + in: query + name: deviceUID + required: false + schema: + items: + type: string + type: array + style: form + - explode: true + in: query + name: sortBy + required: false + schema: + default: captured + enum: + - best_id + - device_serial + - device_uid + - captured + - modified + - device_location + - tower_location + - triangulated_location + - best_location + type: string + style: form + - explode: true + in: query + name: sortOrder + required: false + schema: + default: asc + enum: + - asc + - desc + type: string + style: form + - description: "Start date for filtering results, specified as a Unix timestamp" + example: 1628631763 + explode: true + in: query + name: startDate + required: false + schema: + minimum: 0 + type: integer + style: form + - description: "End date for filtering results, specified as a Unix timestamp" + example: 1657894210 + explode: true + in: query + name: endDate + required: false + schema: + minimum: 0 + type: integer + style: form + - description: "Which date to filter on, either 'captured' or 'uploaded'. This\ + \ will apply to the startDate and endDate parameters" + example: uploaded + explode: true + in: query + name: dateType + required: false + schema: + default: captured + enum: + - captured + - uploaded + type: string + style: form + - explode: true + in: query + name: systemFilesOnly + required: false + schema: + type: boolean + style: form + - example: "_health.qo, data.qo" + explode: true + in: query + name: files + required: false + schema: + type: string + style: form + - description: Response format (JSON or CSV) + explode: true + in: query + name: format + required: false + schema: + default: json + enum: + - json + - csv + type: string + style: form + - description: Filter by Serial Number + explode: true + in: query + name: serialNumber + required: false + schema: + items: + type: string + type: array + style: form + - description: Filter by Session UID + explode: true + in: query + name: sessionUID + required: false + schema: + items: + type: string + type: array + style: form + - description: Filter by Event UID + explode: true + in: query + name: eventUID + required: false + schema: + items: + type: string + type: array + style: form + - description: "Comma-separated list of fields to select from JSON payload (e.g.,\ + \ \"field1,field2.subfield,field3\"), this will reflect the columns in the\ + \ CSV output." + explode: true + in: query + name: selectFields + required: false + schema: + type: string + style: form + - deprecated: true + description: Deprecated. + explode: true + in: query + name: deviceUIDs + required: false + schema: + items: + type: string + type: array + style: form + - deprecated: true + description: Deprecated. + explode: true + in: query + name: since + required: false + schema: + type: string + style: form + responses: + "200": + content: + application/json: + example: + events: + - event: dfa3747d-688b-4250-935b-5dd60354313c + session: b623132c-6afb-4740-bc39-e3634e38f064 + best_id: My Device + device: dev:5c0272311928 + sn: My Device + product: product:com.blues.project.demo + app: app:218f6217-9f78-432e-9fe0-02ca8b5a216c + received: 1.656011227006928E9 + req: note.add + when: 1656010061 + file: air.qo + updates: 1 + body: + humidity: 40.375 + pressure: 97705.66 + temperature: 24.0625 + voltage: 2.598 + best_location_type: triangulated + best_location_when: 1652709545 + best_lat: 34.82476372 + best_lon: -83.32261614 + best_location: Atlanta GA + best_country: US + best_timezone: America/New_York + tower_id: "0,0,0,0" + tri_when: 1652709545 + tri_lat: 34.82475372 + tri_lon: -83.32261614 + tri_location: Atlanta GA + tri_country: US + tri_timezone: America/New_York + tri_points: 6 + has_more: true + schema: + $ref: '#/components/schemas/GetEvents_200_response' + text/csv: + example: !!binary |- + ZXZlbnRVSUQsZGV2aWNlVUlELHdoZW4sYmVzdF9sb2NhdGlvbl90eXBlLGJlc3RfbGF0LGJlc3Rf + bG9uLGJvZHkudGVtcGVyYXR1cmUsYm9keS5odW1pZGl0eQplMTIzNDU2LTc4OTAtYWJjZC1lZjAx + LTIzNDU2Nzg5MGFiYyxkZXY6MDAwMDAwMDAwMDAwMDAxLDE2MjUwOTc2MDAsZ3BzLDM3Ljc3NDks + LTEyMi40MTk0LDIyLjUsNDUuMgpmMjM0NTY3LTg5MDEtYmNkZS1mZzEyLTM0NTY3ODkwMWJjZCxk + ZXY6MDAwMDAwMDAwMDAwMDAyLDE2MjUwOTc2NjAsdHJpYW5ndWxhdGVkLDQwLjcxMjgsLTc0LjAw + NjAsMjQuMyw0OC43CmczNDU2NzgtOTAxMi1jZGVmLWdoMjMtNDU2Nzg5MDEyY2RlLGRldjowMDAw + MDAwMDAwMDAwMDMsMTYyNTA5NzcyMCx0b3dlciw1MS41MDc0LC0wLjEyNzgsMjAuMSw1Mi45Cg== + schema: + format: binary + type: string + description: The response body from a GET events request. + headers: + X-Has-More: + description: True if there are more events + explode: false + schema: + type: boolean + style: simple + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - event + /v1/projects/{projectOrProductUID}/fleets/{fleetUID}/events-cursor: + get: + description: Get Events of a Fleet by cursor + operationId: GetFleetEventsByCursor + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - explode: false + in: path + name: fleetUID + required: true + schema: + type: string + style: simple + - explode: true + in: query + name: limit + required: false + schema: + default: 50 + minimum: 1 + type: integer + style: form + - description: | + A cursor, which can be obtained from the `next_cursor` value from a previous call to this endpoint. The results set returned will include this event as its first result if the given identifier is actually the UID of an event. If this event UID is not found, the parameter is ignored and the results set is the same as if the parameter was not included. + explode: true + in: query + name: cursor + required: false + schema: + type: string + style: form + - explode: true + in: query + name: sortOrder + required: false + schema: + default: asc + enum: + - asc + - desc + type: string + style: form + - explode: true + in: query + name: systemFilesOnly + required: false + schema: + type: boolean + style: form + - example: "_health.qo, data.qo" + explode: true + in: query + name: files + required: false + schema: + type: string + style: form + - description: A Device UID. + explode: true + in: query + name: deviceUID + required: false + schema: + items: + type: string + type: array + style: form + - description: "Start date for filtering results, specified as a Unix timestamp" + example: 1628631763 + explode: true + in: query + name: startDate + required: false + schema: + minimum: 0 + type: integer + style: form + - description: "End date for filtering results, specified as a Unix timestamp" + example: 1657894210 + explode: true + in: query + name: endDate + required: false + schema: + minimum: 0 + type: integer + style: form + responses: + "200": + content: + application/json: + example: + events: + - event: dfa3747d-688b-4250-935b-5dd60354313c + session: b623132c-6afb-4740-bc39-e3634e38f064 + best_id: My Device + device: dev:5c0272311928 + sn: My Device + product: product:com.blues.project.demo + app: app:218f6217-9f78-432e-9fe0-02ca8b5a216c + received: 1.656011227006928E9 + req: note.add + when: 1656010061 + file: air.qo + updates: 1 + body: + humidity: 40.375 + pressure: 97705.66 + temperature: 24.0625 + voltage: 2.598 + best_location_type: triangulated + best_location_when: 1652709545 + best_lat: 34.82476372 + best_lon: -83.32261614 + best_location: Atlanta GA + best_country: US + best_timezone: America/New_York + tower_id: "0,0,0,0" + tri_when: 1652709545 + tri_lat: 34.82475372 + tri_lon: -83.32261614 + tri_location: Atlanta GA + tri_country: US + tri_timezone: America/New_York + tri_points: 6 + next_cursor: "" + has_more: false + schema: + $ref: '#/components/schemas/GetEventsByCursor_200_response' + description: The response body from a GET events by cursor request. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - event + /v1/projects/{projectOrProductUID}/usage/events: + get: + description: "Get events usage for a project with time range and period aggregation,\ + \ when endDate is 0 or unspecified the current time is implied" + operationId: GetEventsUsage + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - description: "Start date for filtering results, specified as a Unix timestamp" + example: 1628631763 + explode: true + in: query + name: startDate + required: false + schema: + minimum: 0 + type: integer + style: form + - description: "End date for filtering results, specified as a Unix timestamp" + example: 1657894210 + explode: true + in: query + name: endDate + required: false + schema: + minimum: 0 + type: integer + style: form + - description: A Device UID. + explode: true + in: query + name: deviceUID + required: false + schema: + items: + type: string + type: array + style: form + - description: Filter by Fleet UID + explode: true + in: query + name: fleetUID + required: false + schema: + items: + type: string + type: array + style: form + - description: Period type for aggregation + explode: true + in: query + name: period + required: true + schema: + enum: + - day + - week + - month + type: string + style: form + - description: Aggregation level for results + explode: true + in: query + name: aggregate + required: false + schema: + items: + enum: + - device + - fleet + - project + - notefile + type: string + type: array + style: form + - description: Filter to specific notefiles + explode: true + in: query + name: notefile + required: false + schema: + items: + type: string + type: array + style: form + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/UsageEventsResponse' + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - usage + /v1/projects/{projectOrProductUID}/usage/route-logs: + get: + description: "Get route logs usage for a project with time range and period\ + \ aggregation, when endDate is 0 or unspecified the current time is implied" + operationId: GetRouteLogsUsage + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - description: "Start date for filtering results, specified as a Unix timestamp" + example: 1628631763 + explode: true + in: query + name: startDate + required: false + schema: + minimum: 0 + type: integer + style: form + - description: "End date for filtering results, specified as a Unix timestamp" + example: 1657894210 + explode: true + in: query + name: endDate + required: false + schema: + minimum: 0 + type: integer + style: form + - description: A Route UID. + explode: true + in: query + name: routeUID + required: false + schema: + items: + type: string + type: array + style: form + - description: Period type for aggregation + explode: true + in: query + name: period + required: true + schema: + enum: + - day + - week + - month + type: string + style: form + - description: Aggregation level for results + explode: true + in: query + name: aggregate + required: false + schema: + default: route + enum: + - route + - project + type: string + style: form + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/GetRouteLogsUsage_200_response' + description: Response body for Route Log Usage + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - usage + /v1/projects/{projectOrProductUID}/usage/sessions: + get: + description: "Get sessions usage for a project with time range and period aggregation,\ + \ when endDate is 0 or unspecified the current time is implied" + operationId: GetSessionsUsage + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - description: "Start date for filtering results, specified as a Unix timestamp" + example: 1628631763 + explode: true + in: query + name: startDate + required: false + schema: + minimum: 0 + type: integer + style: form + - description: "End date for filtering results, specified as a Unix timestamp" + example: 1657894210 + explode: true + in: query + name: endDate + required: false + schema: + minimum: 0 + type: integer + style: form + - description: A Device UID. + explode: true + in: query + name: deviceUID + required: false + schema: + items: + type: string + type: array + style: form + - description: Filter by Fleet UID + explode: true + in: query + name: fleetUID + required: false + schema: + items: + type: string + type: array + style: form + - description: Period type for aggregation + explode: true + in: query + name: period + required: true + schema: + enum: + - day + - week + - month + type: string + style: form + - description: Aggregation level for results + explode: true + in: query + name: aggregate + required: false + schema: + default: device + enum: + - device + - fleet + - project + type: string + style: form + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/GetSessionsUsage_200_response' + description: Response body for Session Usage + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - usage + /v1/projects/{projectOrProductUID}/usage/data: + get: + description: Get data usage in bytes for a project with time range and period + aggregation + operationId: GetDataUsage + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - description: "Start date for filtering results, specified as a Unix timestamp" + example: 1628631763 + explode: true + in: query + name: startDate + required: false + schema: + minimum: 0 + type: integer + style: form + - description: "End date for filtering results, specified as a Unix timestamp" + example: 1657894210 + explode: true + in: query + name: endDate + required: false + schema: + minimum: 0 + type: integer + style: form + - description: A Device UID. + explode: true + in: query + name: deviceUID + required: false + schema: + items: + type: string + type: array + style: form + - description: Filter by Fleet UID + explode: true + in: query + name: fleetUID + required: false + schema: + items: + type: string + type: array + style: form + - description: Period type for aggregation + explode: true + in: query + name: period + required: true + schema: + enum: + - day + - week + - month + type: string + style: form + - description: Aggregation level for results + explode: true + in: query + name: aggregate + required: false + schema: + default: device + enum: + - device + - fleet + - project + type: string + style: form + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/GetDataUsage_200_response' + description: Response body for Data Usage + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - usage + /v1/projects/{projectOrProductUID}/environment_variables: + get: + description: Get environment variables of a project + operationId: GetProjectEnvironmentVariables + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/EnvironmentVariables' + description: The response body from an environment variables request. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - project + put: + description: Set environment variables of a project + operationId: SetProjectEnvironmentVariables + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EnvironmentVariables' + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/EnvironmentVariables' + description: The response body from an environment variables request. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - project + /v1/projects/{projectOrProductUID}/environment_hierarchy: + get: + operationId: GetProjectEnvironmentHierarchy + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/EnvTreeJsonNode' + description: Successfully retrieved project environment hierarchy + "404": + description: Project or device not found + "500": + description: Server error + security: + - personalAccessToken: [] + summary: Get environment variable hierarchy for a device + tags: + - project + /v1/projects/{projectOrProductUID}/environment_variables/{key}: + delete: + description: Delete an environment variable of a project by key + operationId: DeleteProjectEnvironmentVariable + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - description: The environment variable key to delete. + explode: false + in: path + name: key + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/EnvironmentVariables' + description: The response body from an environment variables request. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - project + /v1/projects/{projectOrProductUID}/global-transformation/enable: + post: + description: Enable the project-level event JSONata transformation + operationId: EnableGlobalEventTransformation + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + responses: + "200": + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - project + /v1/projects/{projectOrProductUID}/global-transformation/disable: + post: + description: Disable the project-level event JSONata transformation + operationId: DisableGlobalEventTransformation + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + responses: + "200": + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - project + /v1/projects/{projectOrProductUID}/global-transformation: + post: + description: Set the project-level event JSONata transformation + operationId: SetGlobalEventTransformation + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/JSONata' + description: JSONata expression which will be applied to each event before + it is persisted and routed + required: true + responses: + "200": + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - project + /v1/projects/{projectOrProductUID}/fleets/{fleetUID}/environment_variables: + get: + description: Get environment variables of a fleet + operationId: GetFleetEnvironmentVariables + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - explode: false + in: path + name: fleetUID + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/EnvironmentVariables' + description: The response body from an environment variables request. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - project + put: + description: Set environment variables of a fleet + operationId: SetFleetEnvironmentVariables + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - explode: false + in: path + name: fleetUID + required: true + schema: + type: string + style: simple + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EnvironmentVariables' + description: Environment variables to be added to the fleet + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/EnvironmentVariables' + description: The response body from an environment variables request. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - project + /v1/projects/{projectOrProductUID}/fleets/{fleetUID}/environment_hierarchy: + get: + operationId: GetFleetEnvironmentHierarchy + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - explode: false + in: path + name: fleetUID + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/EnvTreeJsonNode' + description: Successfully retrieved fleet environment hierarchy + "404": + description: Project or device not found + "500": + description: Server error + security: + - personalAccessToken: [] + summary: Get environment variable hierarchy for a device + tags: + - project + /v1/projects/{projectOrProductUID}/fleets/{fleetUID}/environment_variables/{key}: + delete: + description: Delete environment variables of a fleet + operationId: DeleteFleetEnvironmentVariable + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - explode: false + in: path + name: fleetUID + required: true + schema: + type: string + style: simple + - description: The environment variable key to delete. + explode: false + in: path + name: key + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/EnvironmentVariables' + description: The response body from an environment variables request. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - project + /v1/projects/{projectOrProductUID}/devices/{deviceUID}/environment_variables: + get: + description: Get environment variables of a device + operationId: GetDeviceEnvironmentVariables + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/GetDeviceEnvironmentVariables_200_response' + description: The response body from a get device environment variables request. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - device + put: + description: Set environment variables of a device + operationId: SetDeviceEnvironmentVariables + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EnvironmentVariables' + description: Environment variables to be added to the device + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/EnvironmentVariables' + description: The response body from an environment variables request. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - device + /v1/projects/{projectOrProductUID}/devices/{deviceUID}/environment_hierarchy: + get: + operationId: GetDeviceEnvironmentHierarchy + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/EnvTreeJsonNode' + description: Successfully retrieved device environment hierarchy + "404": + description: Project or device not found + "500": + description: Server error + security: + - personalAccessToken: [] + summary: Get environment variable hierarchy for a device + tags: + - device + /v1/projects/{projectOrProductUID}/devices/{deviceUID}/environment_variables/{key}: + delete: + description: Delete environment variable of a device + operationId: DeleteDeviceEnvironmentVariable + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + - description: The environment variable key to delete. + explode: false + in: path + name: key + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/EnvironmentVariables' + description: The response body from an environment variables request. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - device + /v1/products/{productUID}/devices/{deviceUID}/environment_variables_with_pin: + get: + description: Get environment variables of a device with device pin authorization + operationId: GetDeviceEnvironmentVariablesByPin + parameters: + - example: com.blues.bridge:sensors + explode: false + in: path + name: productUID + required: true + schema: + type: string + style: simple + - example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/GetDeviceEnvironmentVariables_200_response' + description: The response body from a get device environment variables request. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - pin: [] + tags: + - device + put: + description: Set environment variables of a device with device pin authorization + operationId: SetDeviceEnvironmentVariablesByPin + parameters: + - example: com.blues.bridge:sensors + explode: false + in: path + name: productUID + required: true + schema: + type: string + style: simple + - example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/EnvironmentVariables' + description: Environment variables to be added to the device + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/EnvironmentVariables' + description: The response body from an environment variables request. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - pin: [] + tags: + - device + /v1/projects/{projectOrProductUID}/devices/{deviceUID}/notes/{notefileID}: + post: + description: "Adds a Note to a Notefile, creating the Notefile if it doesn't\ + \ yet exist." + operationId: AddQiNote + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + - explode: false + in: path + name: notefileID + required: true + schema: + type: string + style: simple + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Note' + description: Body or payload of note to be added to the device + required: true + responses: + "200": + description: An empty object means success + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - device + /v1/projects/{projectOrProductUID}/devices/{deviceUID}/notes/{notefileID}/{noteID}: + delete: + description: Delete a note from a .db notefile + operationId: DeleteDbNote + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + - explode: false + in: path + name: notefileID + required: true + schema: + type: string + style: simple + - explode: false + in: path + name: noteID + required: true + schema: + type: string + style: simple + responses: + "200": + description: An empty object means success + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - device + get: + description: Get a note from a .db notefile + operationId: GetDbNote + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + - explode: false + in: path + name: notefileID + required: true + schema: + type: string + style: simple + - explode: false + in: path + name: noteID + required: true + schema: + type: string + style: simple + - description: Whether to delete the note from the DB notefile + explode: true + in: query + name: delete + required: false + schema: + type: boolean + style: form + - description: Whether to return deleted notes + explode: true + in: query + name: deleted + required: false + schema: + type: boolean + style: form + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/GetDbNote_200_response' + description: The requested note + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - device + post: + description: Add a Note to a .db notefile + operationId: AddDbNote + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + - explode: false + in: path + name: notefileID + required: true + schema: + type: string + style: simple + - explode: false + in: path + name: noteID + required: true + schema: + type: string + style: simple + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Note' + description: Body or payload of note to be added to the device + required: true + responses: + "200": + description: An empty object means success + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - device + put: + description: Update a note in a .db notefile + operationId: UpdateDbNote + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + - explode: false + in: path + name: notefileID + required: true + schema: + type: string + style: simple + - explode: false + in: path + name: noteID + required: true + schema: + type: string + style: simple + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Note' + description: Body or payload of note to be added to the device + required: true + responses: + "200": + description: An empty object means success + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - device + /v1/projects/{projectOrProductUID}/devices/{deviceUID}/notes/{notefileID}/changes: + get: + description: "For .qi files, returns the queued up notes. For .db files, returns\ + \ all notes in the notefile" + operationId: GetNotefile + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + - explode: false + in: path + name: notefileID + required: true + schema: + type: string + style: simple + - description: The change tracker ID. + explode: true + in: query + name: tracker + required: false + schema: + type: string + style: form + - description: The maximum number of Notes to return in the request. + explode: true + in: query + name: max + required: false + schema: + type: integer + style: form + - description: true to reset the tracker to the beginning. + explode: true + in: query + name: start + required: false + schema: + type: boolean + style: form + - description: true to delete the tracker. + explode: true + in: query + name: stop + required: false + schema: + type: boolean + style: form + - description: true to return deleted notes. + explode: true + in: query + name: deleted + required: false + schema: + type: boolean + style: form + - description: true to delete the notes returned by the request. + explode: true + in: query + name: delete + required: false + schema: + type: boolean + style: form + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/GetNotefile_200_response' + description: The note changes object + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - device + /v1/projects/{projectOrProductUID}/devices/{deviceUID}/files/changes: + get: + description: Lists .qi and .db files for the device + operationId: ListNotefiles + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + - description: The change tracker ID. + explode: true + in: query + name: tracker + required: false + schema: + type: string + style: form + - description: One or more files to obtain change information from. + explode: true + in: query + name: files + required: false + schema: + items: + type: string + type: array + style: form + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ListNotefiles_200_response' + description: The notefile changes object + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - device + /v1/projects/{projectOrProductUID}/devices/{deviceUID}/files/changes/pending: + get: + description: Lists .qi and .db files that are pending sync to the Notecard + operationId: ListPendingNotefiles + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/ListPendingNotefiles_200_response' + description: The notefile pending changes object + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - device + /v1/projects/{projectOrProductUID}/devices/{deviceUID}/files: + delete: + description: Deletes Notefiles and the Notes they contain. + operationId: DeleteNotefiles + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DeleteNotefiles_request' + required: true + responses: + "200": + description: An empty object means success + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - device + /v1/projects/{projectOrProductUID}/devices/{deviceUID}/signal: + post: + description: Send a signal from Notehub to a Notecard. + operationId: SignalDevice + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Body' + description: Body or payload of signal to be sent to the device + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/SignalDevice_200_response' + description: A status response. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - device + /v1/projects/{projectOrProductUID}/monitors: + get: + description: Get list of defined Monitors + operationId: GetMonitors + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + items: + $ref: '#/components/schemas/Monitor' + required: + - monitors + type: array + description: The response body from GET /monitors + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - monitor + post: + description: Create a new Monitor + operationId: CreateMonitor + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreateMonitor' + description: Body or payload of monitor to be created + required: true + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/Monitor' + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - monitor + /v1/projects/{projectOrProductUID}/monitors/{monitorUID}: + delete: + description: Delete Monitor + operationId: DeleteMonitor + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: monitor:8bAdf00d-000f-51c-af-01d5eaf00dbad + explode: false + in: path + name: monitorUID + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Monitor' + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - monitor + get: + description: Get Monitor + operationId: GetMonitor + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: monitor:8bAdf00d-000f-51c-af-01d5eaf00dbad + explode: false + in: path + name: monitorUID + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Monitor' + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - monitor + put: + description: Update Monitor + operationId: UpdateMonitor + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: monitor:8bAdf00d-000f-51c-af-01d5eaf00dbad + explode: false + in: path + name: monitorUID + required: true + schema: + type: string + style: simple + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Monitor' + description: Body or payload of monitor to be created + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Monitor' + description: Successful operation + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - monitor + /v1/projects/{projectOrProductUID}/alerts: + get: + description: Get list of defined Alerts + operationId: GetAlerts + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - explode: true + in: query + name: pageSize + required: false + schema: + default: 50 + maximum: 10000 + minimum: 1 + type: integer + style: form + - explode: true + in: query + name: pageNum + required: false + schema: + default: 1 + minimum: 1 + type: integer + style: form + - explode: true + in: query + name: monitorUID + required: false + schema: + type: string + style: form + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/GetAlerts_200_response' + description: The response body from GET /alerts + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - alert + /v1/projects/{projectOrProductUID}/firmware/{firmwareType}/{filename}: + put: + description: Upload firmware binary + operationId: UploadFirmware + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - explode: false + in: path + name: firmwareType + required: true + schema: + enum: + - host + - notecard + type: string + style: simple + - explode: false + in: path + name: filename + required: true + schema: + type: string + style: simple + - description: "Firmware version (optional). If not provided, the version will\ + \ be extracted from firmware binary if available, otherwise left empty" + explode: true + in: query + name: version + required: false + schema: + type: string + style: form + - description: Optional notes describing what's different about this firmware + version + explode: true + in: query + name: notes + required: false + schema: + type: string + style: form + requestBody: + content: + application/octet-stream: + schema: + format: binary + type: string + description: contents of the firmware binary + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/FirmwareInfo' + description: Upload successful + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - project + /v1/projects/{projectOrProductUID}/firmware: + get: + description: Get Available Firmware Information + operationId: GetFirmwareInfo + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - explode: true + in: query + name: product + required: false + schema: + type: string + style: form + - explode: true + in: query + name: firmwareType + required: false + schema: + type: string + style: form + - explode: true + in: query + name: version + required: false + schema: + type: string + style: form + - explode: true + in: query + name: target + required: false + schema: + type: string + style: form + - example: notecard-7.2.2.16518$20240410043100.bin + explode: true + in: query + name: filename + required: false + schema: + type: string + style: form + - explode: true + in: query + name: md5 + required: false + schema: + type: string + style: form + - explode: true + in: query + name: unpublished + required: false + schema: + type: boolean + style: form + responses: + "200": + content: + application/json: + schema: + items: + $ref: '#/components/schemas/FirmwareInfo' + type: array + description: Success + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - project + /v1/projects/{projectOrProductUID}/devices/{deviceUID}/dfu/{firmwareType}/history: + get: + description: Get device DFU history for host or Notecard firmware + operationId: GetDeviceDfuHistory + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + - explode: false + in: path + name: firmwareType + required: true + schema: + enum: + - host + - notecard + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/DeviceDfuHistory' + description: Success + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - project + /v1/projects/{projectOrProductUID}/devices/{deviceUID}/dfu/{firmwareType}/status: + get: + description: Get device DFU history for host or Notecard firmware + operationId: GetDeviceDfuStatus + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + - explode: false + in: path + name: firmwareType + required: true + schema: + enum: + - host + - notecard + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/DeviceDfuStatus' + description: Success + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - project + /v1/projects/{projectOrProductUID}/dfu/{firmwareType}/history: + get: + description: Get host or Notecard DFU history for all devices that match the + filter criteria + operationId: GetDevicesDfuHistory + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - explode: false + in: path + name: firmwareType + required: true + schema: + enum: + - host + - notecard + type: string + style: simple + - explode: true + in: query + name: pageSize + required: false + schema: + default: 50 + maximum: 10000 + minimum: 1 + type: integer + style: form + - explode: true + in: query + name: pageNum + required: false + schema: + default: 1 + minimum: 1 + type: integer + style: form + - explode: true + in: query + name: sortBy + required: false + schema: + default: captured + enum: + - best_id + - device_serial + - device_uid + - captured + - modified + - device_location + - tower_location + - triangulated_location + - best_location + type: string + style: form + - explode: true + in: query + name: sortOrder + required: false + schema: + default: asc + enum: + - asc + - desc + type: string + style: form + - description: A Device UID. + explode: true + in: query + name: deviceUID + required: false + schema: + items: + type: string + type: array + style: form + - description: Tag filter + explode: true + in: query + name: tag + required: false + schema: + items: + type: string + type: array + style: form + - description: Serial number filter + explode: true + in: query + name: serialNumber + required: false + schema: + items: + type: string + type: array + style: form + - explode: true + in: query + name: fleetUID + required: false + schema: + type: string + style: form + - description: Firmware version filter + explode: true + in: query + name: notecardFirmware + required: false + schema: + items: + type: string + type: array + style: form + - description: Location filter + explode: true + in: query + name: location + required: false + schema: + items: + type: string + type: array + style: form + - description: Host firmware filter + explode: true + in: query + name: hostFirmware + required: false + schema: + items: + type: string + type: array + style: form + - explode: true + in: query + name: productUID + required: false + schema: + items: + type: string + type: array + style: form + - description: SKU filter + explode: true + in: query + name: sku + required: false + schema: + items: + type: string + type: array + style: form + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/DeviceDfuHistoryPage' + description: Success + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - project + /v1/projects/{projectOrProductUID}/dfu/{firmwareType}/status: + get: + description: Get host or Notecard DFU history for all devices that match the + filter criteria + operationId: GetDevicesDfuStatus + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - explode: false + in: path + name: firmwareType + required: true + schema: + enum: + - host + - notecard + type: string + style: simple + - explode: true + in: query + name: pageSize + required: false + schema: + default: 50 + maximum: 10000 + minimum: 1 + type: integer + style: form + - explode: true + in: query + name: pageNum + required: false + schema: + default: 1 + minimum: 1 + type: integer + style: form + - explode: true + in: query + name: sortBy + required: false + schema: + default: captured + enum: + - best_id + - device_serial + - device_uid + - captured + - modified + - device_location + - tower_location + - triangulated_location + - best_location + type: string + style: form + - explode: true + in: query + name: sortOrder + required: false + schema: + default: asc + enum: + - asc + - desc + type: string + style: form + - description: A Device UID. + explode: true + in: query + name: deviceUID + required: false + schema: + items: + type: string + type: array + style: form + - description: Tag filter + explode: true + in: query + name: tag + required: false + schema: + items: + type: string + type: array + style: form + - description: Serial number filter + explode: true + in: query + name: serialNumber + required: false + schema: + items: + type: string + type: array + style: form + - explode: true + in: query + name: fleetUID + required: false + schema: + type: string + style: form + - description: Firmware version filter + explode: true + in: query + name: notecardFirmware + required: false + schema: + items: + type: string + type: array + style: form + - description: Location filter + explode: true + in: query + name: location + required: false + schema: + items: + type: string + type: array + style: form + - description: Host firmware filter + explode: true + in: query + name: hostFirmware + required: false + schema: + items: + type: string + type: array + style: form + - explode: true + in: query + name: productUID + required: false + schema: + items: + type: string + type: array + style: form + - description: SKU filter + explode: true + in: query + name: sku + required: false + schema: + items: + type: string + type: array + style: form + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/DeviceDfuStatusPage' + description: Success + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - project + /v1/projects/{projectOrProductUID}/dfu/{firmwareType}/{action}: + post: + description: Update/cancel host or notecard firmware updates + operationId: PerformDfuAction + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - explode: false + in: path + name: firmwareType + required: true + schema: + enum: + - host + - notecard + type: string + style: simple + - explode: false + in: path + name: action + required: true + schema: + enum: + - update + - cancel + type: string + style: simple + - description: A Device UID. + explode: true + in: query + name: deviceUID + required: false + schema: + items: + type: string + type: array + style: form + - description: Tag filter + explode: true + in: query + name: tag + required: false + schema: + items: + type: string + type: array + style: form + - description: Serial number filter + explode: true + in: query + name: serialNumber + required: false + schema: + items: + type: string + type: array + style: form + - explode: true + in: query + name: fleetUID + required: false + schema: + type: string + style: form + - description: Firmware version filter + explode: true + in: query + name: notecardFirmware + required: false + schema: + items: + type: string + type: array + style: form + - description: Location filter + explode: true + in: query + name: location + required: false + schema: + items: + type: string + type: array + style: form + - description: Host firmware filter + explode: true + in: query + name: hostFirmware + required: false + schema: + items: + type: string + type: array + style: form + - explode: true + in: query + name: productUID + required: false + schema: + items: + type: string + type: array + style: form + - description: SKU filter + explode: true + in: query + name: sku + required: false + schema: + items: + type: string + type: array + style: form + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DfuActionRequest' + description: Which firmware in the case of an update action + required: false + responses: + "200": + description: Success + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - project + /v1/repositories: + post: + description: Create a new repository + operationId: CreateRepository + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreateUpdateRepository' + required: true + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/Repository' + description: Repository created successfully + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - repository + /v1/repositories/{repositoryUID}: + delete: + description: Delete a repository + operationId: DeleteRepository + parameters: + - explode: false + in: path + name: repositoryUID + required: true + schema: + example: rid:2606f411-dea6-44a0-9743-1130f57d77d8 + type: string + style: simple + - description: The secret key used to access this repository + explode: false + in: header + name: X-Repository-Key + required: true + schema: + type: string + style: simple + responses: + "200": + description: Dataset deleted successfully + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - repository + get: + description: Get repository information + operationId: GetRepository + parameters: + - explode: false + in: path + name: repositoryUID + required: true + schema: + example: rid:2606f411-dea6-44a0-9743-1130f57d77d8 + type: string + style: simple + - description: The secret key used to access this repository + explode: false + in: header + name: X-Repository-Key + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Repository' + description: Dataset updated successfully + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - repository + put: + description: Update a repository + operationId: PutRepository + parameters: + - explode: false + in: path + name: repositoryUID + required: true + schema: + example: rid:2606f411-dea6-44a0-9743-1130f57d77d8 + type: string + style: simple + - description: The secret key used to access this repository + explode: false + in: header + name: X-Repository-Key + required: true + schema: + type: string + style: simple + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CreateUpdateRepository' + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/Repository' + description: Dataset updated successfully + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - repository + /v1/repositories/{repositoryUID}/datasets: + put: + description: Create a new dataset within a repository + operationId: CreateRepositoryDataset + parameters: + - explode: false + in: path + name: repositoryUID + required: true + schema: + example: rid:2606f411-dea6-44a0-9743-1130f57d77d8 + type: string + style: simple + - description: The secret key used to access this repository + explode: false + in: header + name: X-Repository-Key + required: true + schema: + type: string + style: simple + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DataSet' + required: true + responses: + "201": + content: + application/json: + schema: + $ref: '#/components/schemas/DataSet' + description: Dataset created successfully + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - repository + /v1/repositories/{repositoryUID}/datasets/{name}: + delete: + description: Delete a dataset + operationId: DeleteRepositoryDataset + parameters: + - explode: false + in: path + name: repositoryUID + required: true + schema: + example: rid:2606f411-dea6-44a0-9743-1130f57d77d8 + type: string + style: simple + - description: The name of the data set + explode: false + in: path + name: name + required: true + schema: + type: string + style: simple + - description: The secret key used to access this repository + explode: false + in: header + name: X-Repository-Key + required: true + schema: + type: string + style: simple + responses: + "200": + description: Dataset deleted successfully + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - repository + get: + description: Get the details of a dataset + operationId: GetRepositoryDataset + parameters: + - explode: false + in: path + name: repositoryUID + required: true + schema: + example: rid:2606f411-dea6-44a0-9743-1130f57d77d8 + type: string + style: simple + - description: The name of the data set + explode: false + in: path + name: name + required: true + schema: + type: string + style: simple + - description: The secret key used to access this repository + explode: false + in: header + name: X-Repository-Key + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/DataSet' + description: Dataset details + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - repository + /v1/repositories/{repositoryUID}/sql: + post: + description: Run a raw Clickhouse-compatible SQL statement against the repository's + database. Results are returned in CSV format + operationId: QueryRepositorySql + parameters: + - explode: false + in: path + name: repositoryUID + required: true + schema: + example: rid:2606f411-dea6-44a0-9743-1130f57d77d8 + type: string + style: simple + - description: The secret key used to access this repository + explode: false + in: header + name: X-Repository-Key + required: true + schema: + type: string + style: simple + - description: "Specify the format of the response data. This functions the\ + \ same as the ClickHouse `FORMAT` clause. Supported values include `CSV`,\ + \ `JSON`, `JSONEachRow`, `TabSeparated`, and `NDJSON`. If not specified,\ + \ defaults to `TabSeparated`." + explode: false + in: header + name: X-ClickHouse-Format + required: false + schema: + type: string + style: simple + requestBody: + content: + text/plain: + schema: + type: string + description: Clickhouse-compatible SQL statement + required: true + responses: + "200": + content: + text/plain: + schema: + format: binary + type: string + description: Successful query + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - repository + /v1/repositories/{repositoryUID}/datasets/{name}/query: + get: + description: "Query a dataset with support for time ranges, field selection,\ + \ filtering, and location-based queries" + operationId: QueryRepositoryDataset + parameters: + - description: The secret key used to access this repository + explode: false + in: header + name: X-Repository-Key + required: true + schema: + type: string + style: simple + - explode: false + in: path + name: repositoryUID + required: true + schema: + example: rid:2606f411-dea6-44a0-9743-1130f57d77d8 + type: string + style: simple + - description: The name of the data set + explode: false + in: path + name: name + required: true + schema: + type: string + style: simple + - description: "Start of the time range, as an ISO-8601 date or relative to\ + \ now (e.g. -1y). Relative dates follow the Postgres INTERVAL format." + explode: true + in: query + name: start + required: true + schema: + type: string + style: form + - description: "End of the time range, as an ISO-8601 date or relative to now.\ + \ If omitted, current time is used." + explode: true + in: query + name: end + required: false + schema: + type: string + style: form + - description: "Comma separated list of fields to include. Supports aggregate\ + \ functions (avg, sum, min, max, count, most_recent)." + explode: true + in: query + name: select + required: false + schema: + type: string + style: form + - description: "Additional filters using boolean logic mini-language (e.g. and.(device.eq.dev:123,temp.gt.100))" + explode: true + in: query + name: where + required: false + schema: + type: string + style: form + - description: "Aggregate results into buckets for a time duration, expressed\ + \ in Postgres INTERVAL format" + explode: true + in: query + name: aggregate_window + required: false + schema: + type: string + style: form + - description: "Latitude and Longitude for location-based filtering, location_near_radius\ + \ must also be provided" + explode: true + in: query + name: location_near + required: false + schema: + example: "42.393125,-71.185015" + type: string + style: form + - description: "Distance from location_near in meters, location_near must also\ + \ be provided" + explode: true + in: query + name: location_near_radius + required: false + schema: + type: integer + style: form + - description: Limit the number of results returned + explode: true + in: query + name: limit + required: false + schema: + type: integer + style: form + - description: Order the results by a field + explode: true + in: query + name: order_by + required: false + schema: + type: string + style: form + - description: Return only distinct results + explode: true + in: query + name: distinct + required: false + schema: + type: boolean + style: form + responses: + "200": + content: + text/csv: + example: !!binary |- + bGF0LGxvbixkZXZpY2Usc2Vzc2lvbl9iZWdhbgo0Mi4zOTMxMjUsLTcxLjE4NTAxNSxkZXY6MTIz + NCwyMDI0LTAxLTAxVDEyOjM0OjU2Wgo= + schema: + format: binary + type: string + description: Successful query + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - repository + /v1/repositories/{repositoryUID}/data: + get: + description: Get event and session data from a repository in NDJSON format. + operationId: GetRepositoryData + parameters: + - description: The secret key used to access this repository + explode: false + in: header + name: X-Repository-Key + required: true + schema: + type: string + style: simple + - explode: false + in: path + name: repositoryUID + required: true + schema: + example: rid:2606f411-dea6-44a0-9743-1130f57d77d8 + type: string + style: simple + - description: "Start of the time range, as an ISO-8601 date or relative to\ + \ now (e.g. -1y). Relative dates follow the Postgres INTERVAL format." + explode: true + in: query + name: start + required: true + schema: + type: string + style: form + - description: "End of the time range, as an ISO-8601 date or relative to now.\ + \ If omitted, current time is used." + explode: true + in: query + name: end + required: false + schema: + type: string + style: form + responses: + "200": + content: + text/csv: + schema: + format: binary + type: string + description: Successful request returning NDJSON encoded event and session + objects. + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + security: + - personalAccessToken: [] + tags: + - repository + /v1/projects/{projectOrProductUID}/schemas: + get: + operationId: GetNotefileSchemas + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + items: + $ref: '#/components/schemas/NotefileSchema' + type: array + description: List of notefile schema definitions + summary: Get variable format for a notefile + tags: + - project + /v1/projects/{projectOrProductUID}/webhooks: + get: + description: Retrieves all webhooks for the specified project + operationId: GetWebhooks + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + responses: + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/GetWebhooks_200_response' + description: Webhooks retrieved successfully + security: + - personalAccessToken: [] + tags: + - webhook + /v1/projects/{projectOrProductUID}/webhooks/{webhookUID}: + delete: + description: Deletes the specified webhook + operationId: DeleteWebhook + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - description: Webhook UID + explode: false + in: path + name: webhookUID + required: true + schema: + example: Abc_123-2646f411-dc56-44a0-9743-4130f47a74h8 + type: string + style: simple + responses: + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + "200": + description: Webhook deleted successfully + security: + - personalAccessToken: [] + tags: + - webhook + get: + description: Retrieves the configuration settings for the specified webhook + operationId: GetWebhook + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - description: Webhook UID + explode: false + in: path + name: webhookUID + required: true + schema: + example: Abc_123-2646f411-dc56-44a0-9743-4130f47a74h8 + type: string + style: simple + responses: + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/WebhookSettings' + description: Webhook settings retrieved successfully + security: + - personalAccessToken: [] + tags: + - webhook + post: + description: Creates a webhook for the specified product with the given name. + The name | must be unique within the project. + operationId: CreateWebhook + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - description: Webhook UID + explode: false + in: path + name: webhookUID + required: true + schema: + example: Abc_123-2646f411-dc56-44a0-9743-4130f47a74h8 + type: string + style: simple + requestBody: + content: + application/json: + example: + settings: + disabled: false + id: Abc_123-2646f411-dc56-44a0-9743-4130f47a74h8 + transform: "{\"device\":body.end_device_ids.dev_eui,\"sn\":body.end_device_ids.device_id,\"\ + body\":body.uplink_message.decoded_payload,\"details\":body}" + schema: + $ref: '#/components/schemas/WebhookSettings' + required: true + responses: + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + "200": + description: Webhook created successfully + security: + - personalAccessToken: [] + tags: + - webhook + put: + description: Updates the configuration settings for the specified webhook. | + Webhook will be created if it does not exist. Update body will completely + replace the existing settings. + operationId: UpdateWebhook + parameters: + - example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + - description: Webhook UID + explode: false + in: path + name: webhookUID + required: true + schema: + example: Abc_123-2646f411-dc56-44a0-9743-4130f47a74h8 + type: string + style: simple + requestBody: + content: + application/json: + example: + disabled: false + transform: "{\"device\":body.end_device_ids.dev_eui,\"sn\":body.end_device_ids.device_id,\"\ + body\":body.uplink_message.decoded_payload,\"details\":body}" + schema: + $ref: '#/components/schemas/WebhookSettings' + required: true + responses: + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + "200": + description: Webhook updated successfully + security: + - personalAccessToken: [] + tags: + - webhook + /v1/products/{productUID}/ext-devices/{deviceUID}/event: + post: + description: Creates an event using specified webhook + operationId: CreateEventExtDevice + parameters: + - example: com.blues.bridge:sensors + explode: false + in: path + name: productUID + required: true + schema: + type: string + style: simple + - example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Event' + description: Event Object + required: true + responses: + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + "200": + description: Event Created Successfully + security: + - personalAccessToken: [] + tags: + - external devices + /v1/products/{productUID}/ext-devices/{deviceUID}/session/open: + post: + description: Create a Session for the specified device. | If a session is currently + open it will be closed and a new one opened. + operationId: ExtDeviceSessionOpen + parameters: + - example: com.blues.bridge:sensors + explode: false + in: path + name: productUID + required: true + schema: + type: string + style: simple + - example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DeviceSession' + description: Session Object + required: true + responses: + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + "200": + description: Session Created Successfully + security: + - personalAccessToken: [] + tags: + - external devices + /v1/products/{productUID}/ext-devices/{deviceUID}/session/close: + post: + description: Closes the session for the specified device if open + operationId: ExtDeviceSessionClose + parameters: + - example: com.blues.bridge:sensors + explode: false + in: path + name: productUID + required: true + schema: + type: string + style: simple + - example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DeviceSession' + description: Session Object + required: true + responses: + default: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + "200": + description: Session closed + security: + - personalAccessToken: [] + tags: + - external devices + /v1/personal-access-tokens: + get: + operationId: ListPersonalAccessTokens + responses: + "200": + content: + application/json: + schema: + items: + $ref: '#/components/schemas/PersonalAccessToken' + type: array + description: A list of personal access tokens + security: + - personalAccessToken: [] + summary: List Personal Access Tokens + tags: + - api_access + post: + operationId: CreatePersonalAccessToken + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PersonalAccessTokenInfo' + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/PersonalAccessTokenSecret' + description: New personal access token and secret + "404": + description: Personal access token not found + "400": + description: Invalid input + security: + - personalAccessToken: [] + summary: Create new personal access token + tags: + - api_access + /v1/personal-access-tokens/{patUID}: + delete: + operationId: DeletePersonalAccessToken + parameters: + - description: UID of the personal access token + explode: false + in: path + name: patUID + required: true + schema: + type: string + style: simple + responses: + "204": + description: personal access token was deleted + "404": + description: personal access token not found + security: + - personalAccessToken: [] + summary: Delete an personal access token by UID + tags: + - api_access + get: + operationId: GetPersonalAccessToken + parameters: + - description: UID of the API key + explode: false + in: path + name: patUID + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/PersonalAccessToken' + description: A single API key + "404": + description: API key not found + security: + - personalAccessToken: [] + summary: Get API key by UID + tags: + - api_access + put: + operationId: UpdatePersonalAccessToken + parameters: + - description: UID of the personal access token to update + explode: false + in: path + name: patUID + required: true + schema: + type: string + style: simple + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PersonalAccessTokenInfo' + required: true + responses: + "200": + content: + application/json: + schema: + $ref: '#/components/schemas/PersonalAccessToken' + description: Updated personal access token + "404": + description: Personal access token not found + "400": + description: Invalid input + security: + - personalAccessToken: [] + summary: Update personal access token key information + tags: + - api_access +components: + parameters: + projectOrProductUIDParam: + example: app:2606f411-dea6-44a0-9743-1130f57d77d8 + explode: false + in: path + name: projectOrProductUID + required: true + schema: + type: string + style: simple + productUIDParam: + example: com.blues.bridge:sensors + explode: false + in: path + name: productUID + required: true + schema: + type: string + style: simple + eventUIDParam: + example: 4506f411-dea6-44a0-9743-1130f57d7747 + explode: false + in: path + name: eventUID + required: true + schema: + type: string + style: simple + fleetUIDParam: + explode: false + in: path + name: fleetUID + required: true + schema: + type: string + style: simple + fleetUIDsParam: + explode: true + in: query + name: fleetUIDs + required: false + schema: + items: + type: string + type: array + style: form + deviceTagsParam: + explode: true + in: query + name: deviceTags + required: false + schema: + items: + type: string + type: array + style: form + deviceUIDParam: + example: dev:000000000000000 + explode: false + in: path + name: deviceUID + required: true + schema: + type: string + style: simple + firmwareTypeParam: + explode: false + in: path + name: firmwareType + required: true + schema: + enum: + - host + - notecard + type: string + style: simple + dfuActionParam: + explode: false + in: path + name: action + required: true + schema: + enum: + - update + - cancel + type: string + style: simple + notefileIDParam: + explode: false + in: path + name: notefileID + required: true + schema: + type: string + style: simple + routeUIDParam: + example: route:cbd20093cba58392c9f9bbdd0cdeb1a0 + explode: false + in: path + name: routeUID + required: true + schema: + type: string + style: simple + noteIDParam: + explode: false + in: path + name: noteID + required: true + schema: + type: string + style: simple + monitorUIDParam: + example: monitor:8bAdf00d-000f-51c-af-01d5eaf00dbad + explode: false + in: path + name: monitorUID + required: true + schema: + type: string + style: simple + repositoryUIDParam: + explode: false + in: path + name: repositoryUID + required: true + schema: + example: rid:2606f411-dea6-44a0-9743-1130f57d77d8 + type: string + style: simple + datasetNameParam: + description: The name of the data set + explode: false + in: path + name: name + required: true + schema: + type: string + style: simple + datasetStartQueryParam: + description: "Start of the time range, as an ISO-8601 date or relative to now\ + \ (e.g. -1y). Relative dates follow the Postgres INTERVAL format." + explode: true + in: query + name: start + required: true + schema: + type: string + style: form + datasetEndQueryParam: + description: "End of the time range, as an ISO-8601 date or relative to now.\ + \ If omitted, current time is used." + explode: true + in: query + name: end + required: false + schema: + type: string + style: form + datasetSelectQueryParam: + description: "Comma separated list of fields to include. Supports aggregate\ + \ functions (avg, sum, min, max, count, most_recent)." + explode: true + in: query + name: select + required: false + schema: + type: string + style: form + datasetWhereQueryParam: + description: "Additional filters using boolean logic mini-language (e.g. and.(device.eq.dev:123,temp.gt.100))" + explode: true + in: query + name: where + required: false + schema: + type: string + style: form + datasetAggregateWindowQueryParam: + description: "Aggregate results into buckets for a time duration, expressed\ + \ in Postgres INTERVAL format" + explode: true + in: query + name: aggregate_window + required: false + schema: + type: string + style: form + datasetLocationNearQueryParam: + description: "Latitude and Longitude for location-based filtering, location_near_radius\ + \ must also be provided" + explode: true + in: query + name: location_near + required: false + schema: + example: "42.393125,-71.185015" + type: string + style: form + datasetLocationRadiusQueryParam: + description: "Distance from location_near in meters, location_near must also\ + \ be provided" + explode: true + in: query + name: location_near_radius + required: false + schema: + type: integer + style: form + datasetLimitQueryParam: + description: Limit the number of results returned + explode: true + in: query + name: limit + required: false + schema: + type: integer + style: form + datasetOrderByQueryParam: + description: Order the results by a field + explode: true + in: query + name: order_by + required: false + schema: + type: string + style: form + datasetDistinctQueryParam: + description: Return only distinct results + explode: true + in: query + name: distinct + required: false + schema: + type: boolean + style: form + pageSizeParam: + explode: true + in: query + name: pageSize + required: false + schema: + default: 50 + maximum: 10000 + minimum: 1 + type: integer + style: form + pageNumParam: + explode: true + in: query + name: pageNum + required: false + schema: + default: 1 + minimum: 1 + type: integer + style: form + deviceUIDParamQuery: + description: A Device UID. + explode: true + in: query + name: deviceUID + required: false + schema: + items: + type: string + type: array + style: form + deviceUIDsParamQuery: + description: An array of Device UIDs. + explode: true + in: query + name: deviceUIDs + required: false + schema: + items: + type: string + type: array + style: form + routeUIDParamQuery: + description: A Route UID. + explode: true + in: query + name: routeUID + required: false + schema: + items: + type: string + type: array + style: form + sortByParam: + explode: true + in: query + name: sortBy + required: false + schema: + default: captured + enum: + - best_id + - device_serial + - device_uid + - captured + - modified + - device_location + - tower_location + - triangulated_location + - best_location + type: string + style: form + routeLogsSortByParam: + explode: true + in: query + name: sortBy + required: false + schema: + default: date + enum: + - date + - event + type: string + style: form + routeLogsSortOrderParam: + explode: true + in: query + name: sortOrder + required: false + schema: + default: desc + enum: + - asc + - desc + type: string + style: form + sortOrderParam: + explode: true + in: query + name: sortOrder + required: false + schema: + default: asc + enum: + - asc + - desc + type: string + style: form + startDateParam: + description: "Start date for filtering results, specified as a Unix timestamp" + example: 1628631763 + explode: true + in: query + name: startDate + required: false + schema: + minimum: 0 + type: integer + style: form + endDateParam: + description: "End date for filtering results, specified as a Unix timestamp" + example: 1657894210 + explode: true + in: query + name: endDate + required: false + schema: + minimum: 0 + type: integer + style: form + dateTypeParam: + description: "Which date to filter on, either 'captured' or 'uploaded'. This\ + \ will apply to the startDate and endDate parameters" + example: uploaded + explode: true + in: query + name: dateType + required: false + schema: + default: captured + enum: + - captured + - uploaded + type: string + style: form + cursorParam: + description: | + A cursor, which can be obtained from the `next_cursor` value from a previous call to this endpoint. The results set returned will include this event as its first result if the given identifier is actually the UID of an event. If this event UID is not found, the parameter is ignored and the results set is the same as if the parameter was not included. + explode: true + in: query + name: cursor + required: false + schema: + type: string + style: form + limitParam: + explode: true + in: query + name: limit + required: false + schema: + default: 50 + minimum: 1 + type: integer + style: form + systemFilesOnlyParam: + explode: true + in: query + name: systemFilesOnly + required: false + schema: + type: boolean + style: form + mostRecentOnlyParam: + explode: true + in: query + name: mostRecentOnly + required: false + schema: + type: boolean + style: form + fleetUIDQueryParam: + explode: true + in: query + name: fleetUID + required: false + schema: + type: string + style: form + fleetUIDsQueryParam: + explode: true + in: query + name: fleetUID + required: false + schema: + items: + type: string + type: array + style: form + filesQueryParam: + example: "_health.qo, data.qo" + explode: true + in: query + name: files + required: false + schema: + type: string + style: form + formatParam: + description: Response format (JSON or CSV) + explode: true + in: query + name: format + required: false + schema: + default: json + enum: + - json + - csv + type: string + style: form + serialNumberFilterParam: + description: Filter by Serial Number + explode: true + in: query + name: serialNumber + required: false + schema: + items: + type: string + type: array + style: form + fleetUIDFilterQueryParam: + description: Filter by Fleet UID + explode: true + in: query + name: fleetUID + required: false + schema: + items: + type: string + type: array + style: form + sessionUIDFilterParam: + description: Filter by Session UID + explode: true + in: query + name: sessionUID + required: false + schema: + items: + type: string + type: array + style: form + eventUIDFilterQueryParam: + description: Filter by Event UID + explode: true + in: query + name: eventUID + required: false + schema: + items: + type: string + type: array + style: form + selectFieldsParam: + description: "Comma-separated list of fields to select from JSON payload (e.g.,\ + \ \"field1,field2.subfield,field3\"), this will reflect the columns in the\ + \ CSV output." + explode: true + in: query + name: selectFields + required: false + schema: + type: string + style: form + filenameQueryParam: + example: notecard-7.2.2.16518$20240410043100.bin + explode: true + in: query + name: filename + required: false + schema: + type: string + style: form + md5QueryParam: + explode: true + in: query + name: md5 + required: false + schema: + type: string + style: form + unpublishedQueryParam: + explode: true + in: query + name: unpublished + required: false + schema: + type: boolean + style: form + versionQueryParam: + explode: true + in: query + name: version + required: false + schema: + type: string + style: form + targetQueryParam: + explode: true + in: query + name: target + required: false + schema: + type: string + style: form + firmwareTypeQueryParam: + explode: true + in: query + name: firmwareType + required: false + schema: + type: string + style: form + productUIDQueryParam: + explode: true + in: query + name: productUID + required: false + schema: + items: + type: string + type: array + style: form + productQueryParam: + explode: true + in: query + name: product + required: false + schema: + type: string + style: form + monitorUIDQueryParam: + explode: true + in: query + name: monitorUID + required: false + schema: + type: string + style: form + tagParam: + description: Tag filter + explode: true + in: query + name: tag + required: false + schema: + items: + type: string + type: array + style: form + serialNumberParam: + description: Serial number filter + explode: true + in: query + name: serialNumber + required: false + schema: + items: + type: string + type: array + style: form + notecardFirmwareParam: + description: Firmware version filter + explode: true + in: query + name: notecardFirmware + required: false + schema: + items: + type: string + type: array + style: form + locationParam: + description: Location filter + explode: true + in: query + name: location + required: false + schema: + items: + type: string + type: array + style: form + hostFirmwareParam: + description: Host firmware filter + explode: true + in: query + name: hostFirmware + required: false + schema: + items: + type: string + type: array + style: form + skuParam: + description: SKU filter + explode: true + in: query + name: sku + required: false + schema: + items: + type: string + type: array + style: form + repositoryKey: + description: The secret key used to access this repository + explode: false + in: header + name: X-Repository-Key + required: true + schema: + type: string + style: simple + webhookUIDParam: + description: Webhook UID + explode: false + in: path + name: webhookUID + required: true + schema: + example: Abc_123-2646f411-dc56-44a0-9743-4130f47a74h8 + type: string + style: simple + responses: + ErrorResponse: + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + description: The response body in case of an API error. + EventsResponse: + content: + application/json: + example: + events: + - event: dfa3747d-688b-4250-935b-5dd60354313c + session: b623132c-6afb-4740-bc39-e3634e38f064 + best_id: My Device + device: dev:5c0272311928 + sn: My Device + product: product:com.blues.project.demo + app: app:218f6217-9f78-432e-9fe0-02ca8b5a216c + received: 1.656011227006928E9 + req: note.add + when: 1656010061 + file: air.qo + updates: 1 + body: + humidity: 40.375 + pressure: 97705.66 + temperature: 24.0625 + voltage: 2.598 + best_location_type: triangulated + best_location_when: 1652709545 + best_lat: 34.82476372 + best_lon: -83.32261614 + best_location: Atlanta GA + best_country: US + best_timezone: America/New_York + tower_id: "0,0,0,0" + tri_when: 1652709545 + tri_lat: 34.82475372 + tri_lon: -83.32261614 + tri_location: Atlanta GA + tri_country: US + tri_timezone: America/New_York + tri_points: 6 + has_more: true + schema: + $ref: '#/components/schemas/GetEvents_200_response' + text/csv: + example: !!binary |- + ZXZlbnRVSUQsZGV2aWNlVUlELHdoZW4sYmVzdF9sb2NhdGlvbl90eXBlLGJlc3RfbGF0LGJlc3Rf + bG9uLGJvZHkudGVtcGVyYXR1cmUsYm9keS5odW1pZGl0eQplMTIzNDU2LTc4OTAtYWJjZC1lZjAx + LTIzNDU2Nzg5MGFiYyxkZXY6MDAwMDAwMDAwMDAwMDAxLDE2MjUwOTc2MDAsZ3BzLDM3Ljc3NDks + LTEyMi40MTk0LDIyLjUsNDUuMgpmMjM0NTY3LTg5MDEtYmNkZS1mZzEyLTM0NTY3ODkwMWJjZCxk + ZXY6MDAwMDAwMDAwMDAwMDAyLDE2MjUwOTc2NjAsdHJpYW5ndWxhdGVkLDQwLjcxMjgsLTc0LjAw + NjAsMjQuMyw0OC43CmczNDU2NzgtOTAxMi1jZGVmLWdoMjMtNDU2Nzg5MDEyY2RlLGRldjowMDAw + MDAwMDAwMDAwMDMsMTYyNTA5NzcyMCx0b3dlciw1MS41MDc0LC0wLjEyNzgsMjAuMSw1Mi45Cg== + schema: + format: binary + type: string + description: The response body from a GET events request. + headers: + X-Has-More: + description: True if there are more events + explode: false + schema: + type: boolean + style: simple + EventsByCursorResponse: + content: + application/json: + example: + events: + - event: dfa3747d-688b-4250-935b-5dd60354313c + session: b623132c-6afb-4740-bc39-e3634e38f064 + best_id: My Device + device: dev:5c0272311928 + sn: My Device + product: product:com.blues.project.demo + app: app:218f6217-9f78-432e-9fe0-02ca8b5a216c + received: 1.656011227006928E9 + req: note.add + when: 1656010061 + file: air.qo + updates: 1 + body: + humidity: 40.375 + pressure: 97705.66 + temperature: 24.0625 + voltage: 2.598 + best_location_type: triangulated + best_location_when: 1652709545 + best_lat: 34.82476372 + best_lon: -83.32261614 + best_location: Atlanta GA + best_country: US + best_timezone: America/New_York + tower_id: "0,0,0,0" + tri_when: 1652709545 + tri_lat: 34.82475372 + tri_lon: -83.32261614 + tri_location: Atlanta GA + tri_country: US + tri_timezone: America/New_York + tri_points: 6 + next_cursor: "" + has_more: false + schema: + $ref: '#/components/schemas/GetEventsByCursor_200_response' + description: The response body from a GET events by cursor request. + EnvironmentVariablesResponse: + content: + application/json: + schema: + $ref: '#/components/schemas/EnvironmentVariables' + description: The response body from an environment variables request. + GetDeviceEnvironmentVariablesResponse: + content: + application/json: + schema: + $ref: '#/components/schemas/GetDeviceEnvironmentVariables_200_response' + description: The response body from a get device environment variables request. + FleetsResponse: + content: + application/json: + schema: + $ref: '#/components/schemas/GetFleets_200_response' + description: The response body from a fleets endpoint. + DevicesResponse: + content: + application/json: + schema: + $ref: '#/components/schemas/GetDevices_200_response' + description: List of Devices + NoteResponse: + content: + application/json: + schema: + properties: + total: + description: The total number of notes active on the Notefile. + type: integer + template: + description: true when a template is active on the Notefile. + type: boolean + type: object + description: The response body from a note endpoint. + SessionResponse: + content: + application/json: + example: + sessions: + - session: d76689be-37cd-423c-b695-7e0c19a2a264 + device: dev:000000000000000 + product: product:com.blues.demo:project + fleets: + - fleet:46be9834-5te6-42c1-0000-b5ea05e248d7 + cell: "310,410,17169,77315594" + rssi: -61 + sinr: 183 + rsrp: -91 + rsrq: -13 + bars: 2 + rat: lte + bearer: LTE FDD + ip: 10.68.56.193 + iccid: "89011704278500000000" + apn: a-notehub.com.attz + tower: + time: 1667250835 + "n": Shorewood Hills WI + c: US + lat: 43.0742625 + lon: -89.44239062499999 + zone: America/Chicago + mcc: 310 + mnc: 410 + lac: 17169 + cid: 77315594 + l: 86MG3HF5+P25 + count: 7 + towers: 1 + tri: {} + when: 1667251044 + voltage: 4.174 + temp: 24.437 + continuous: true + tls: true + work: 1667251046 + events: 14 + moved: 1667250807 + orientation: face-up + trigger: first sync; continuous connection mode + hp_secs_total: 7659 + hp_secs_data: 7659 + hp_cycles_total: 3 + hp_cycles_data: 3 + period: + since: 1667250832 + duration: 215 + bytes_rcvd: 2501 + bytes_sent: 4138 + sessions_tls: 1 + notes_sent: 12 + has_more: true + schema: + $ref: '#/components/schemas/GetDeviceSessions_200_response' + description: The response body for a session request. + LatestResponse: + content: + application/json: + example: + latest_events: + - event: 81bd2bf1-0399-4978-bc46-8f779b4af350 + session: ed18884b-f2a6-419f-b856-d28dc8f0892b + tls: true + device: dev:864475040523995 + product: product:com.blues.app:myapp + app: app:2e49f10a-76a9-4e2d-8b18-cef0b8b46446 + received: 1.669667707564694E9 + req: session.begin + when: 1669667707 + file: _session.qo + body: + why: sensors.qo requested sync (sensors.qo) (TLS) + tower_when: 1669667691 + tower_lat: 43.769062500000004 + tower_lon: -83.657359375 + tower_country: US + tower_location: Waverly MI + tower_timezone: America/Detroit + tower_id: "310,410,20483,184692495" + - event: 916d4c81-06ae-4263-9b55-7a3a0f73cb5a + session: 28cdc39f-9f62-4789-b0a3-2f35f9448ced + device: dev:864475040523995 + sn: tj-1 + product: product:com.blues.app:myapp + app: app:2e49f10a-76a9-4e2d-8b18-cef0b8b46446 + received: 1.669667713221659E9 + req: note.add + when: 1669667689 + file: data.qo + body: + humid: 56.23 + temp: 35.5 + tower_when: 1669667677 + tower_lat: 43.769062500000004 + tower_lon: -83.657359375 + tower_country: US + tower_location: Waverly MI + tower_timezone: America/Detroit + tower_id: "310,410,20483,184692495" + - event: e98c2c3b-edbe-4fe7-af57-2196cc843eb7 + session: 7211392c-6895-43f8-9256-790655348be5 + device: dev:864475040523995 + product: product:com.blues.app:myapp + app: app:2e49f10a-76a9-4e2d-8b18-cef0b8b46446 + received: 1.66966771185316E9 + req: note.add + when: 1669667695 + file: sensors.qo + body: + humidity: 69.88647200683693 + pressure: 993.6294496104914 + temp: 21.273027181770885 + tower_when: 1669667689 + tower_lat: 43.747037500000005 + tower_lon: -83.665859375 + tower_country: US + tower_location: Waverly MI + tower_timezone: America/Detroit + tower_id: "310,410,20483,184692496" + schema: + $ref: '#/components/schemas/GetDeviceLatestEvents_200_response' + description: The response body for a Latest Events request. + MonitorsResponse: + content: + application/json: + schema: + items: + $ref: '#/components/schemas/Monitor' + required: + - monitors + type: array + description: The response body from GET /monitors + AlertsResponse: + content: + application/json: + schema: + $ref: '#/components/schemas/GetAlerts_200_response' + description: The response body from GET /alerts + DevicePlansResponse: + content: + application/json: + schema: + $ref: '#/components/schemas/GetDevicePlans_200_response' + description: Response body for /plans + UsageSessionsResponse: + content: + application/json: + schema: + $ref: '#/components/schemas/GetSessionsUsage_200_response' + description: Response body for Session Usage + UsageRouteLogsResponse: + content: + application/json: + schema: + $ref: '#/components/schemas/GetRouteLogsUsage_200_response' + description: Response body for Route Log Usage + UsageDataResponse: + content: + application/json: + schema: + $ref: '#/components/schemas/GetDataUsage_200_response' + description: Response body for Data Usage + schemas: + Error: + example: + request: request + code: 323 + debug: debug + err: err + details: "{}" + status: status + properties: + err: + description: Human readable error message. + type: string + code: + description: The HTTP error code associated with the error. + maximum: 599 + minimum: 300 + type: integer + status: + description: Machine readable representation of the HTTP error code. + type: string + request: + type: string + details: + type: object + debug: + type: string + required: + - code + - err + - status + type: object + PersonalAccessToken: + example: + uid: uid + expires_at: 2000-01-23T04:56:07.000+00:00 + last_used: 2000-01-23T04:56:07.000+00:00 + name: name + description: description + created_at: 2000-01-23T04:56:07.000+00:00 + created_by: + uid: uid + name: name + email: email + suspended: true + properties: + uid: + description: Unique and public identifier + type: string + name: + description: Name for this API Key + type: string + description: + description: Optional description for this API Key + type: string + created_by: + $ref: '#/components/schemas/PersonalAccessToken_created_by' + expires_at: + description: When the key expires + format: date-time + nullable: true + type: string + created_at: + description: When the key was created + format: date-time + type: string + last_used: + description: "When it was last used, if ever" + format: date-time + nullable: true + type: string + suspended: + description: "if true, this token cannot be used" + type: boolean + type: object + PersonalAccessTokenSecret: + example: + uid: uid + secret: secret + properties: + uid: + description: Unique and public identifier + type: string + secret: + description: The secret + type: string + type: object + PersonalAccessTokenInfo: + example: + expires_at: 2000-01-23T04:56:07.000+00:00 + name: name + description: description + suspended: true + properties: + name: + type: string + description: + type: string + expires_at: + description: New expiration timestamp for the personal access token + format: date-time + nullable: true + type: string + suspended: + description: "if true, the token is temporarily suspended" + type: boolean + required: + - expiresAt + type: object + OAuth2TokenResponse: + example: + access_token: access_token + scope: openid + token_type: bearer + expires_in: 1799 + properties: + access_token: + description: The issued access token + type: string + token_type: + description: Usually 'bearer' + example: bearer + type: string + expires_in: + description: Lifetime in seconds of the access token. + example: 1799 + type: integer + scope: + description: Granted scopes (space-delimited). + example: openid + type: string + required: + - access_token + - expires_in + - token_type + type: object + OAuth2Error: + example: + error_description: error_description + error: invalid_request + properties: + error: + description: RFC 6749 error code. + enum: + - invalid_request + - invalid_client + - invalid_grant + - unauthorized_client + - unsupported_grant_type + - invalid_scope + type: string + error_description: + description: Human-readable explanation of the error. + type: string + required: + - error + type: object + UsageEventsResponse: + example: + data: + - total_events: 42 + notefiles: + _log.qo: 20 + session.qo: 50 + customer1.qo: 1 + fleet: fleet:1042ddc5-3b2c-4cec-b1fb-d3040538094d + period: 2025-07-23T00:00:00Z + watchdog_events: 10 + platform_events: 15 + device: dev:123456789012345 + - total_events: 42 + notefiles: + _log.qo: 20 + session.qo: 50 + customer1.qo: 1 + fleet: fleet:1042ddc5-3b2c-4cec-b1fb-d3040538094d + period: 2025-07-23T00:00:00Z + watchdog_events: 10 + platform_events: 15 + device: dev:123456789012345 + properties: + data: + items: + $ref: '#/components/schemas/UsageEventsData' + type: array + required: + - data + type: object + UsageEventsData: + example: + total_events: 42 + notefiles: + _log.qo: 20 + session.qo: 50 + customer1.qo: 1 + fleet: fleet:1042ddc5-3b2c-4cec-b1fb-d3040538094d + period: 2025-07-23T00:00:00Z + watchdog_events: 10 + platform_events: 15 + device: dev:123456789012345 + properties: + device: + example: dev:123456789012345 + type: string + fleet: + example: fleet:1042ddc5-3b2c-4cec-b1fb-d3040538094d + type: string + period: + example: 2025-07-23T00:00:00Z + format: date-time + type: string + total_events: + description: "Total events the device sent to notehub, including associated\ + \ notehub generated events" + example: 42 + type: integer + platform_events: + description: "Total platform events. Platform events are _log, _session,\ + \ _health, and _geolocate events some of which are send from the device,\ + \ some generated by notehub. These events are not billed." + example: 15 + type: integer + watchdog_events: + description: "Watchdog events are events generated by notehub when a watchdog\ + \ timer is configured for a device to indicate is has not been online\ + \ for a period of time. These events are billed but should not be used\ + \ to indicate a device is active, or connected, at this time." + example: 10 + type: integer + notefiles: + additionalProperties: + type: integer + description: Count of events per notefile. Only present when aggregate=notefile + is specified. + example: + _log.qo: 20 + session.qo: 50 + customer1.qo: 1 + type: object + required: + - period + - platform_events + - total_events + - watchdog_events + type: object + UsageRouteLogsData: + example: + period: 2025-07-23T00:00:00Z + failed_routes: 4 + route: route:cbd20093cba58392c9f9bbdd0cdeb1a0 + successful_routes: 38 + total_routes: 42 + properties: + route: + description: The route UID (only present when aggregate is 'route') + example: route:cbd20093cba58392c9f9bbdd0cdeb1a0 + type: string + period: + example: 2025-07-23T00:00:00Z + format: date-time + type: string + successful_routes: + example: 38 + type: integer + failed_routes: + example: 4 + type: integer + total_routes: + example: 42 + type: integer + required: + - failed_routes + - period + - successful_routes + - total_routes + type: object + UsageSessionsData: + example: + fleet: fleet:1042ddc5-3b2c-4cec-b1fb-d3040538094d + period: 2025-07-23T00:00:00Z + sessions: 12 + total_bytes: 1048576 + device: dev:123456789012345 + properties: + device: + example: dev:123456789012345 + type: string + fleet: + example: fleet:1042ddc5-3b2c-4cec-b1fb-d3040538094d + type: string + period: + example: 2025-07-23T00:00:00Z + format: date-time + type: string + sessions: + example: 12 + type: integer + total_bytes: + example: 1048576 + type: integer + required: + - period + - sessions + - total_bytes + type: object + JSONata: + type: object + BillingAccount: + example: + uid: uid + role: billing_admin + name: name + properties: + uid: + type: string + name: + type: string + role: + $ref: '#/components/schemas/BillingAccountRole' + required: + - name + - role + - uid + type: object + BillingAccountRole: + enum: + - billing_admin + - billing_manager + - project_creator + type: string + Role: + enum: + - owner + - developer + - viewer + - support + - null + nullable: true + type: string + Project: + example: + uid: uid + role: owner + technical_contact: + role: role + organization: organization + name: name + email: email + administrative_contact: + role: role + organization: organization + name: name + email: email + created: 2000-01-23T04:56:07.000+00:00 + label: label + properties: + uid: + type: string + label: + type: string + created: + format: date-time + type: string + role: + $ref: '#/components/schemas/Role' + administrative_contact: + $ref: '#/components/schemas/Contact' + technical_contact: + $ref: '#/components/schemas/Contact' + required: + - created + - label + - uid + type: object + ProjectMember: + example: + role: owner + name: name + email: email + properties: + name: + type: string + email: + description: | + The email address of the project member. + This property will only be populated if the viewer is an owner of the project. + type: string + role: + $ref: '#/components/schemas/Role' + required: + - email + - name + - role + type: object + Contact: + example: + role: role + organization: organization + name: name + email: email + nullable: true + properties: + name: + type: string + email: + type: string + role: + type: string + organization: + type: string + type: object + Product: + example: + uid: uid + disable_devices_by_default: true + auto_provision_fleets: + - auto_provision_fleets + - auto_provision_fleets + label: label + properties: + uid: + type: string + label: + type: string + auto_provision_fleets: + items: + type: string + nullable: true + type: array + disable_devices_by_default: + type: boolean + required: + - disable_devices_by_default + - label + - uid + type: object + FleetRule: + description: "JSONata expression that will be evaluated to determine device\ + \ membership into this fleet, if the expression evaluates to a 1, the device\ + \ will be included, if it evaluates to -1 it will be removed, and if it evaluates\ + \ to 0 or errors it will be left unchanged." + type: string + FleetConnectivityAssurance: + example: + enabled: true + nullable: true + properties: + enabled: + description: Whether Connectivity Assurance is enabled for this fleet + type: boolean + required: + - enabled + type: object + Fleet: + example: + uid: uid + environment_variables: + key: environment_variables + watchdog_mins: 0 + created: 2000-01-23T04:56:07.000+00:00 + connectivity_assurance: + enabled: true + smart_rule: smart_rule + label: label + properties: + uid: + description: Fleet UID + type: string + label: + description: Fleet label + type: string + created: + description: RFC3339 timestamp in UTC + format: date-time + type: string + environment_variables: + additionalProperties: + type: string + description: The environment variables for this device that have been set + using the Notehub API or UI. + type: object + smart_rule: + description: "JSONata expression that will be evaluated to determine device\ + \ membership into this fleet, if the expression evaluates to a 1, the\ + \ device will be included, if it evaluates to -1 it will be removed, and\ + \ if it evaluates to 0 or errors it will be left unchanged." + type: string + connectivity_assurance: + $ref: '#/components/schemas/FleetConnectivityAssurance' + watchdog_mins: + description: A watchdog timer is used to generate an event every N minutes + of inactivity. 0 means no watchdog + format: int64 + type: integer + required: + - created + - label + - uid + type: object + DFUState: + example: + read: 1.0246457001441578 + began: 4.145608029883936 + length: 3.616076749251911 + type: card + version: version + mode: idle + file: file + crc32: 2.027123023002322 + updated: 1.4894159098541704 + retry: 7.386281948385884 + errors: 1.2315135367772556 + md5: md5 + status: status + nullable: true + properties: + type: + enum: + - card + - user + type: string + file: + description: Firmware filename + type: string + length: + description: Length of firmware file + type: number + crc32: + description: Used for image verification + type: number + md5: + description: Used for image verification + type: string + mode: + description: | + * "idle" - nothing downloading or downloaded + * "error" - halted and in the error state + * "downloading" - transferring data from cloud to module + * "sideloading" - transferring data via request to module + * "ready" - DFU data is ready/verified and waiting on external storage + * "ready-retry" - DFU data is ready/verified and retrying + * "updating" - currently updating + * "completed" - DFU is done successfully + enum: + - idle + - error + - downloading + - sideloading + - ready + - ready-retry + - updating + - completed + type: string + status: + description: Status message + type: string + began: + description: The time when the DFU began + type: number + retry: + description: Value of _fw_retry environment var at time of DFU initialization + type: number + errors: + description: The number of consecutive errors the DFU process has encountered + type: number + read: + description: The amount the notecard has read of the image from notehub + type: number + updated: + description: Last updated timestamp + type: number + version: + description: "Last known version, which is generally a JSON object contained\ + \ within the firmware image" + type: string + type: object + DFUEnv: + example: + user: + read: 1.0246457001441578 + began: 4.145608029883936 + length: 3.616076749251911 + type: card + version: version + mode: idle + file: file + crc32: 2.027123023002322 + updated: 1.4894159098541704 + retry: 7.386281948385884 + errors: 1.2315135367772556 + md5: md5 + status: status + card: + read: 1.0246457001441578 + began: 4.145608029883936 + length: 3.616076749251911 + type: card + version: version + mode: idle + file: file + crc32: 2.027123023002322 + updated: 1.4894159098541704 + retry: 7.386281948385884 + errors: 1.2315135367772556 + md5: md5 + status: status + nullable: true + properties: + card: + $ref: '#/components/schemas/DFUState' + user: + $ref: '#/components/schemas/DFUState' + type: object + Device: + example: + cellular_usage: + - iccid: iccid + last_updated: 4 + limit: 7 + used: 6 + lifetime_used: 1 + - iccid: iccid + last_updated: 4 + limit: 7 + used: 6 + lifetime_used: 1 + fleet_uids: + - fleet_uids + - fleet_uids + serial_number: serial_number + tower_info: + mnc: 6 + mcc: 0 + cell_id: 5 + lac: 1 + tower_location: + country: country + timezone: timezone + latitude: 5.637376656633329 + name: name + when: when + longitude: 2.3021358869347655 + triangulated_location: + country: country + timezone: timezone + latitude: 5.637376656633329 + name: name + when: when + longitude: 2.3021358869347655 + voltage: 7.061401241503109 + gps_location: + country: country + timezone: timezone + latitude: 5.637376656633329 + name: name + when: when + longitude: 2.3021358869347655 + uid: uid + dfu: + user: + read: 1.0246457001441578 + began: 4.145608029883936 + length: 3.616076749251911 + type: card + version: version + mode: idle + file: file + crc32: 2.027123023002322 + updated: 1.4894159098541704 + retry: 7.386281948385884 + errors: 1.2315135367772556 + md5: md5 + status: status + card: + read: 1.0246457001441578 + began: 4.145608029883936 + length: 3.616076749251911 + type: card + version: version + mode: idle + file: file + crc32: 2.027123023002322 + updated: 1.4894159098541704 + retry: 7.386281948385884 + errors: 1.2315135367772556 + md5: md5 + status: status + last_activity: 2000-01-23T04:56:07.000+00:00 + provisioned: 2000-01-23T04:56:07.000+00:00 + contact: + role: role + organization: organization + name: name + email: email + firmware_host: firmware_host + temperature: 9.301444243932576 + disabled: true + product_uid: product_uid + sku: sku + firmware_notecard: firmware_notecard + properties: + uid: + type: string + serial_number: + type: string + provisioned: + format: date-time + type: string + last_activity: + format: date-time + nullable: true + type: string + contact: + $ref: '#/components/schemas/Contact' + product_uid: + type: string + fleet_uids: + items: + type: string + type: array + tower_info: + $ref: '#/components/schemas/Device_tower_info' + tower_location: + $ref: '#/components/schemas/Location' + gps_location: + $ref: '#/components/schemas/Location' + triangulated_location: + $ref: '#/components/schemas/Location' + voltage: + format: double + type: number + temperature: + format: double + type: number + dfu: + $ref: '#/components/schemas/DFUEnv' + firmware_host: + type: string + firmware_notecard: + type: string + sku: + type: string + disabled: + type: boolean + cellular_usage: + items: + $ref: '#/components/schemas/SimUsage' + type: array + required: + - fleet_uids + - product_uid + - provisioned + - temperature + - uid + - voltage + type: object + Event: + example: + moved: 7.457744773683766 + rsrp: 5.025004791520295 + where_when: 7.061401241503109 + body: "{}" + when: 6.027456183070403 + where_timezone: where_timezone + tri_country: tri_country + tri_points: 6.84685269835264 + where_lon: 3.616076749251911 + best_lat: 5.637376656633329 + best_timezone: best_timezone + payload: payload + where_country: where_country + tri_timezone: tri_timezone + tower_timezone: tower_timezone + sku: sku + tower_lat: 4.145608029883936 + req: req + app: app + tower_id: tower_id + where_lat: 9.301444243932576 + rat: rat + best_lon: 2.3021358869347655 + received: 0.8008281904610115 + where_olc: where_olc + tower_location: tower_location + tower_country: tower_country + sinr: 4.965218492984954 + device: device + note: note + tri_lat: 1.0246457001441578 + session: session + bssid: bssid + best_location: best_location + updates: 1.4658129805029452 + ssid: ssid + best_id: best_id + ordering_code: ordering_code + file: file + best_country: best_country + sn: sn + event: event + tower_when: 2.027123023002322 + best_location_when: 5.962133916683182 + product: product + where_location: where_location + tower_lon: 7.386281948385884 + orientation: orientation + rssi: 1.1730742509559433 + temp: 8.762042012749001 + tri_when: 1.2315135367772556 + tri_location: tri_location + transport: transport + best_location_type: best_location_type + bars: 9.369310271410669 + voltage: 6.683562403749608 + environment: "{}" + rsrq: 9.965781217890562 + tri_lon: 1.4894159098541704 + tls: true + properties: + event: + description: Event UID (globally unique) + type: string + session: + description: Session UID (globally unique) + type: string + tls: + description: Whether TLS was used on the connection between the device and + notehub. Only available on _session.qo events. + type: boolean + transport: + description: "The transport used for this event, e.g., \"cellular\", \"\ + wifi\", \", etc." + type: string + best_id: + description: "The device serial number, or the DeviceUID if the serial number\ + \ is not set" + type: string + device: + description: Device UID (globally unique) + type: string + sn: + description: The device serial number + type: string + product: + description: Product UID (globally unique) + type: string + app: + description: App UID (globally unique) + type: string + received: + description: The unix timestamp when the event was received + format: double + type: number + req: + description: The notecard request + type: string + when: + description: When the event was captured on the device + type: number + file: + description: The notefile associated with this event + type: string + note: + description: The note ID in the notefile + type: string + updates: + type: number + body: + description: A JSON object containing event details + type: object + payload: + description: A base64-encoded binary payload + type: string + best_location_type: + description: "One of \"gps\", \"triangulated\", or \"tower\"" + type: string + best_location_when: + description: Unix timestamp + type: number + best_lat: + description: Latitude + format: double + type: number + best_lon: + description: Longitude + format: double + type: number + best_location: + description: Location + type: string + best_country: + description: Country + type: string + best_timezone: + description: Timezone + type: string + where_olc: + description: Open Location Code + type: string + where_when: + description: Unix timestamp + type: number + where_lat: + description: Latitude + format: double + type: number + where_lon: + description: Longitude + format: double + type: number + where_location: + description: Location + type: string + where_country: + description: Country + type: string + where_timezone: + description: Timezone + type: string + tower_when: + description: Unix timestamp + type: number + tower_lat: + description: Latitude + format: double + type: number + tower_lon: + description: Longitude + format: double + type: number + tower_country: + description: Country + type: string + tower_location: + description: Location + type: string + tower_timezone: + description: Timezone + type: string + tower_id: + description: Tower ID + type: string + tri_when: + description: Unix timestamp + type: number + tri_lat: + description: Latitude + format: double + type: number + tri_lon: + description: Longitude + format: double + type: number + tri_location: + description: Location + type: string + tri_country: + description: Country + type: string + tri_timezone: + description: Timezone + type: string + tri_points: + description: Triangulation points + type: number + moved: + description: The number of times the device was sensed to have moved between + the last session and this session. Only available on _session.qo events. + type: number + orientation: + description: The orientation of the device. Only available on _session.qo + events. + type: string + rssi: + description: Received Signal Strength Indicator (RSSI) is an estimated measurement + of how well a device can receive signals. Only available on _session.qo + events. + type: number + sinr: + description: SINR. Only available on _session.qo events. + type: number + rsrp: + description: RSRP. Only available on _session.qo events. + type: number + rsrq: + description: RSRQ. Only available on _session.qo events. + type: number + rat: + description: Rat. Only available on _session.qo events. + type: string + bars: + description: Bars. Only available on _session.qo events. + type: number + voltage: + description: Device voltage. Only available on _session.qo events. + format: double + type: number + temp: + description: Device temperature. Only available on _session.qo events. + format: double + type: number + environment: + description: Routed environment variables beginning with "$". Only available + on _session.qo events. + type: object + sku: + description: SKU. Only available on _session.qo events. + type: string + ordering_code: + description: Ordering code. Only available on _session.qo events. + type: string + ssid: + description: SSID. Only available on _session.qo events. + type: string + bssid: + description: BSSID. Only available on _session.qo events. + type: string + type: object + Location: + example: + country: country + timezone: timezone + latitude: 5.637376656633329 + name: name + when: when + longitude: 2.3021358869347655 + nullable: true + properties: + when: + type: string + name: + type: string + country: + type: string + timezone: + type: string + latitude: + format: double + type: number + longitude: + format: double + type: number + required: + - country + - latitude + - longitude + - name + - timezone + - when + type: object + TowerLocation: + example: + c: c + mnc: 7 + lon: 2.027123023002322 + source: source + mcc: 4 + l: l + "n": "n" + lac: 1 + towers: 6 + zone: zone + z: 1 + time: 9 + lat: 3.616076749251911 + cid: 1 + properties: + source: + description: The source of this location + type: string + time: + description: Unix timestamp when this location was ascertained + format: int64 + type: integer + "n": + description: Name of the location + type: string + c: + description: Country code + type: string + lat: + description: Latitude + format: double + type: number + lon: + description: Longitude + format: double + type: number + zone: + description: Timezone name + type: string + mcc: + description: Mobile Country Code + type: integer + mnc: + description: Mobile Network Code + type: integer + lac: + description: Location Area Code + type: integer + cid: + description: Cell ID + type: integer + l: + description: Open Location Code + type: string + z: + description: Timezone ID + type: integer + towers: + description: Number of triangulation points + type: integer + type: object + DeviceSession: + example: + moved: 9 + rsrp: 5 + where_when: 1 + when: 7 + where_timezone: where_timezone + where_lon: 5.025004791520295 + iccid: iccid + penalty_secs: 8 + where_country: where_country + continuous: true + bearer: bearer + hp_secs_total: 6 + apn: apn + events: 8 + tower: + c: c + mnc: 7 + lon: 2.027123023002322 + source: source + mcc: 4 + l: l + "n": "n" + lac: 1 + towers: 6 + zone: zone + z: 1 + time: 9 + lat: 3.616076749251911 + cid: 1 + triangulate: "{}" + power_primary: true + period: + duration: 5 + sessions_tls: 7 + notes_sent: 0 + bytes_rcvd: 6 + bytes_rcvd_secondary: 3 + sessions_tcp: 0 + notes_rcvd: 6 + bytes_sent_secondary: 7 + bytes_sent: 3 + since: 6 + rat: rat + where_lat: 4.965218492984954 + work: 6 + ip: ip + tri: + c: c + mnc: 7 + lon: 2.027123023002322 + source: source + mcc: 4 + l: l + "n": "n" + lac: 1 + towers: 6 + zone: zone + z: 1 + time: 9 + lat: 3.616076749251911 + cid: 1 + why_session_closed: why_session_closed + hp_secs_data: 3 + sinr: 5 + power_usb: true + power_charging: true + session_began: 0 + device: device + why_session_opened: why_session_opened + failed_connects: 7 + session: session + bssid: bssid + scan: scan + fleets: + - fleets + - fleets + cell: cell + ssid: ssid + usage_actual: true + hp_cycles_data: 2 + hp_secs_gps: 6 + where: where + sn: sn + product: product + rssi: 1 + where_location: where_location + temp: 9.369310271410669 + orientation: orientation + transport: transport + bars: 7 + voltage: 9.965781217890562 + hp_cycles_gps: 6 + rsrq: 2 + tls: true + hp_cycles_total: 1 + session_ended: 6 + power_mah: 4.652396432933246 + properties: + session: + description: Session UID + type: string + session_began: + description: UNIX timestamp of session start + format: int64 + type: integer + why_session_opened: + description: Reason for session opening + type: string + session_ended: + description: UNIX timestamp of session end + format: int64 + type: integer + why_session_closed: + description: Reason for session closing + type: string + device: + description: Device UID + type: string + sn: + description: Device Serial Number + type: string + product: + description: Product UID + type: string + fleets: + description: Array of Fleet UIDs + items: + type: string + type: array + cell: + description: "Cell ID where the session originated and quality (\"mcc,mnc,lac,cellid\"\ + )" + type: string + scan: + format: byte + type: string + triangulate: + type: object + rssi: + format: int + type: integer + sinr: + format: int + type: integer + rsrp: + format: int + type: integer + rsrq: + format: int + type: integer + bars: + format: int + type: integer + rat: + type: string + bearer: + type: string + ip: + type: string + bssid: + type: string + ssid: + type: string + iccid: + type: string + apn: + type: string + transport: + description: Type of network transport + type: string + tower: + $ref: '#/components/schemas/TowerLocation' + tri: + $ref: '#/components/schemas/TowerLocation' + when: + description: Last known capture time of a note routed through this session + in Unix timestamp + format: int64 + type: integer + where_when: + description: Unix timestamp of last GPS location + format: int64 + type: integer + where: + description: Open Location Code from last GPS location + type: string + where_lat: + format: double + type: number + where_lon: + format: double + type: number + where_location: + type: string + where_country: + type: string + where_timezone: + type: string + usage_actual: + type: boolean + voltage: + format: double + type: number + temp: + format: double + type: number + continuous: + description: Was this a continuous connection? + type: boolean + tls: + description: Was TLS used? + type: boolean + work: + description: Unix timestamp of the last time work was done for this session + format: int64 + type: integer + events: + description: Number of events routed + format: int64 + type: integer + moved: + format: int64 + type: integer + orientation: + type: string + hp_secs_total: + description: Total number of seconds in high power mode + format: int64 + type: integer + hp_secs_data: + format: int64 + type: integer + hp_secs_gps: + format: int64 + type: integer + hp_cycles_total: + format: int64 + type: integer + hp_cycles_data: + format: int64 + type: integer + hp_cycles_gps: + format: int64 + type: integer + period: + $ref: '#/components/schemas/DeviceUsage' + power_charging: + type: boolean + power_usb: + type: boolean + power_primary: + type: boolean + power_mah: + format: double + type: number + penalty_secs: + description: Number of seconds in penalty in the prior session + format: int64 + type: integer + failed_connects: + description: Number of failed connection attempts in the prior session + format: int64 + type: integer + type: object + DeviceUsage: + example: + duration: 5 + sessions_tls: 7 + notes_sent: 0 + bytes_rcvd: 6 + bytes_rcvd_secondary: 3 + sessions_tcp: 0 + notes_rcvd: 6 + bytes_sent_secondary: 7 + bytes_sent: 3 + since: 6 + properties: + since: + description: Unix timestamp + format: int64 + type: integer + duration: + description: Duration in seconds + format: int64 + type: integer + bytes_rcvd: + format: int64 + type: integer + bytes_sent: + format: int64 + type: integer + bytes_rcvd_secondary: + format: int64 + type: integer + bytes_sent_secondary: + format: int64 + type: integer + sessions_tcp: + format: int64 + type: integer + sessions_tls: + format: int64 + type: integer + notes_rcvd: + format: int64 + type: integer + notes_sent: + format: int64 + type: integer + type: object + NotehubRoute: + additionalProperties: false + description: Route resource as stored/returned by the server. + example: + s3archive: + throttle_ms: 9 + key_id: key_id + bucket_name: bucket_name + fleets: + - fleets + - fleets + file_access: file_access + timeout: 6 + url: https://openapi-generator.tech + filter: + files: + - env.qo + - motion.qo + type: inclusion + system_notefiles: false + archive_every_mins: 9 + file_folder: file_folder + transform: + jsonata_in: $.body.data + jsonata: $.body.data + format: json + bucket_region: bucket_region + archive_count_exceeds: 8 + key_secret: key_secret + archive_id: archive_id + file_format: file_format + bucket_endpoint: bucket_endpoint + snowflake: + filter: + files: + - env.qo + - motion.qo + type: inclusion + system_notefiles: false + transform: + jsonata_in: $.body.data + jsonata: $.body.data + format: json + user_name: user_name + account_name: account_name + private_key_name: private_key_name + pem: pem + fleets: + - fleets + - fleets + organization_name: organization_name + timeout: 6 + radnote: + throttle_ms: 4 + event_id: 2 + test_api: true + fleets: + - fleets + - fleets + client_secret: client_secret + client_id: client_id + data_feed_key: data_feed_key + google: + filter: + files: + - env.qo + - motion.qo + type: inclusion + system_notefiles: false + transform: + jsonata_in: $.body.data + jsonata: $.body.data + format: json + throttle_ms: 1 + fleets: + - fleets + - fleets + url: https://openapi-generator.tech + timeout: 5 + token: token + label: label + datacake: + filter: + files: + - env.qo + - motion.qo + type: inclusion + system_notefiles: false + transform: + jsonata_in: $.body.data + jsonata: $.body.data + format: json + throttle_ms: 6 + http_headers: + key: http_headers + disable_http_headers: true + fleets: + - fleets + - fleets + url: https://openapi-generator.tech + timeout: 3 + type: type + thingworx: + filter: + files: + - env.qo + - motion.qo + type: inclusion + system_notefiles: false + transform: + jsonata_in: $.body.data + jsonata: $.body.data + format: json + throttle_ms: 1 + app_key: app_key + fleets: + - fleets + - fleets + url: https://openapi-generator.tech + timeout: 1 + uid: uid + proxy: + transform: + jsonata_in: $.body.data + jsonata: $.body.data + format: json + http_headers: + key: http_headers + alias: alias + fleets: + - fleets + - fleets + url: https://openapi-generator.tech + timeout: 5 + mqtt: + throttle_ms: 2 + certificate: certificate + certificate_name: certificate_name + private_key_name: private_key_name + fleets: + - fleets + - fleets + broker: broker + timeout: 7 + filter: + files: + - env.qo + - motion.qo + type: inclusion + system_notefiles: false + transform: + jsonata_in: $.body.data + jsonata: $.body.data + format: json + password: password + port: port + topic: topic + key: key + username: username + slack: + filter: + files: + - env.qo + - motion.qo + type: inclusion + system_notefiles: false + transform: + jsonata_in: $.body.data + jsonata: $.body.data + format: json + throttle_ms: 5 + webhook_url: https://openapi-generator.tech + blocks: blocks + channel: channel + bearer: bearer + fleets: + - fleets + - fleets + text: text + timeout: 9 + modified: 2000-01-23T04:56:07.000+00:00 + http: + filter: + files: + - env.qo + - motion.qo + type: inclusion + system_notefiles: false + transform: + jsonata_in: $.body.data + jsonata: $.body.data + format: json + throttle_ms: 0 + http_headers: + key: http_headers + disable_http_headers: true + fleets: + - fleets + - fleets + url: https://openapi-generator.tech + timeout: 6 + snowpipe_streaming: + filter: + files: + - env.qo + - motion.qo + type: inclusion + system_notefiles: false + pipe_name: pipe_name + transform: + jsonata_in: $.body.data + jsonata: $.body.data + format: json + database_name: database_name + user_name: user_name + account_name: account_name + private_key_name: private_key_name + pem: pem + fleets: + - fleets + - fleets + organization_name: organization_name + schema_name: schema_name + timeout: 7 + disabled: false + blynk: + filter: + files: + - env.qo + - motion.qo + type: inclusion + system_notefiles: false + transform: + jsonata_in: $.body.data + jsonata: $.body.data + format: json + throttle_ms: 6 + fleets: + - fleets + - fleets + region: region + timeout: 1 + qubitro: + filter: + files: + - env.qo + - motion.qo + type: inclusion + system_notefiles: false + transform: + jsonata_in: $.body.data + jsonata: $.body.data + format: json + throttle_ms: 2 + webhook_signing_key: webhook_signing_key + project_id: project_id + fleets: + - fleets + - fleets + timeout: 6 + aws: + access_key_id: access_key_id + throttle_ms: 9 + disable_http_headers: true + channel: channel + fleets: + - fleets + - fleets + url: https://openapi-generator.tech + timeout: 3 + filter: + files: + - env.qo + - motion.qo + type: inclusion + system_notefiles: false + transform: + jsonata_in: $.body.data + jsonata: $.body.data + format: json + message_deduplication_id: message_deduplication_id + http_headers: + key: http_headers + access_key_secret: access_key_secret + message_group_id: message_group_id + region: region + twilio: + filter: + files: + - env.qo + - motion.qo + type: inclusion + system_notefiles: false + throttle_ms: 4 + account_sid: account_sid + fleets: + - fleets + - fleets + from: from + to: to + auth_token: auth_token + message: message + timeout: 1 + azure: + filter: + files: + - env.qo + - motion.qo + type: inclusion + system_notefiles: false + transform: + jsonata_in: $.body.data + jsonata: $.body.data + format: json + throttle_ms: 7 + fleets: + - fleets + - fleets + sas_policy_name: sas_policy_name + functions_key_secret: functions_key_secret + url: https://openapi-generator.tech + timeout: 1 + sas_policy_key: sas_policy_key + properties: + uid: + readOnly: true + type: string + label: + type: string + type: + description: Mirrors hublib.RouteType. + type: string + modified: + format: date-time + readOnly: true + type: string + disabled: + default: false + type: boolean + http: + $ref: '#/components/schemas/HttpRoute' + google: + $ref: '#/components/schemas/GoogleRoute' + proxy: + $ref: '#/components/schemas/ProxyRoute' + mqtt: + $ref: '#/components/schemas/MqttRoute' + aws: + $ref: '#/components/schemas/AwsRoute' + radnote: + $ref: '#/components/schemas/RadRoute' + azure: + $ref: '#/components/schemas/AzureRoute' + thingworx: + $ref: '#/components/schemas/ThingworxRoute' + snowflake: + $ref: '#/components/schemas/SnowflakeRoute' + snowpipe_streaming: + $ref: '#/components/schemas/SnowpipeStreamingRoute' + twilio: + $ref: '#/components/schemas/TwilioRoute' + slack: + $ref: '#/components/schemas/SlackRoute' + s3archive: + $ref: '#/components/schemas/S3ArchiveRoute' + datacake: + $ref: '#/components/schemas/DatacakeRoute' + blynk: + $ref: '#/components/schemas/BlynkRoute' + qubitro: + $ref: '#/components/schemas/QubitroRoute' + type: object + Filter: + description: | + Filter applied to route data. Controls which notefiles are sent through the route. + example: + files: + - env.qo + - motion.qo + type: inclusion + system_notefiles: false + properties: + type: + description: Type of filter to apply (corresponds to `hublib.NotefileFilterType`). + example: inclusion + type: string + system_notefiles: + description: Whether system notefiles should be included. + example: false + type: boolean + files: + description: List of notefile names or patterns to filter on. + example: + - env.qo + - motion.qo + items: + type: string + type: array + type: object + RouteTransformSettings: + description: | + Settings for transforming route payloads before delivery. Supports format + selection and JSONata-based transformations. + example: + jsonata_in: $.body.data + jsonata: $.body.data + format: json + properties: + format: + description: "Output format for transformed data (e.g., \"json\", \"xml\"\ + , \"text\")." + example: json + type: string + jsonata: + description: JSONata expression used to transform the data payload (outgoing). + example: $.body.data + type: string + jsonata_in: + description: JSONata expression used to transform the data payload (incoming). + example: $.body.data + type: string + type: object + HttpRoute: + additionalProperties: false + example: + filter: + files: + - env.qo + - motion.qo + type: inclusion + system_notefiles: false + transform: + jsonata_in: $.body.data + jsonata: $.body.data + format: json + throttle_ms: 0 + http_headers: + key: http_headers + disable_http_headers: true + fleets: + - fleets + - fleets + url: https://openapi-generator.tech + timeout: 6 + properties: + fleets: + description: "If non-empty, applies only to the listed fleets." + items: + type: string + type: array + filter: + $ref: '#/components/schemas/Filter' + transform: + $ref: '#/components/schemas/RouteTransformSettings' + throttle_ms: + type: integer + url: + format: uri + type: string + http_headers: + additionalProperties: + type: string + type: object + disable_http_headers: + type: boolean + timeout: + type: integer + type: object + GoogleRoute: + additionalProperties: false + example: + filter: + files: + - env.qo + - motion.qo + type: inclusion + system_notefiles: false + transform: + jsonata_in: $.body.data + jsonata: $.body.data + format: json + throttle_ms: 1 + fleets: + - fleets + - fleets + url: https://openapi-generator.tech + timeout: 5 + token: token + properties: + fleets: + items: + type: string + type: array + token: + type: string + filter: + $ref: '#/components/schemas/Filter' + transform: + $ref: '#/components/schemas/RouteTransformSettings' + throttle_ms: + type: integer + url: + format: uri + type: string + timeout: + type: integer + type: object + ProxyRoute: + additionalProperties: false + example: + transform: + jsonata_in: $.body.data + jsonata: $.body.data + format: json + http_headers: + key: http_headers + alias: alias + fleets: + - fleets + - fleets + url: https://openapi-generator.tech + timeout: 5 + properties: + fleets: + items: + type: string + type: array + url: + format: uri + type: string + alias: + type: string + http_headers: + additionalProperties: + type: string + type: object + timeout: + type: integer + transform: + $ref: '#/components/schemas/RouteTransformSettings' + type: object + MqttRoute: + additionalProperties: false + example: + throttle_ms: 2 + certificate: certificate + certificate_name: certificate_name + private_key_name: private_key_name + fleets: + - fleets + - fleets + broker: broker + timeout: 7 + filter: + files: + - env.qo + - motion.qo + type: inclusion + system_notefiles: false + transform: + jsonata_in: $.body.data + jsonata: $.body.data + format: json + password: password + port: port + topic: topic + key: key + username: username + properties: + fleets: + items: + type: string + type: array + filter: + $ref: '#/components/schemas/Filter' + transform: + $ref: '#/components/schemas/RouteTransformSettings' + throttle_ms: + type: integer + broker: + type: string + port: + type: string + username: + type: string + password: + format: password + type: string + topic: + type: string + timeout: + type: integer + certificate: + type: string + certificate_name: + type: string + key: + type: string + private_key_name: + type: string + type: object + AwsRoute: + additionalProperties: false + example: + access_key_id: access_key_id + throttle_ms: 9 + disable_http_headers: true + channel: channel + fleets: + - fleets + - fleets + url: https://openapi-generator.tech + timeout: 3 + filter: + files: + - env.qo + - motion.qo + type: inclusion + system_notefiles: false + transform: + jsonata_in: $.body.data + jsonata: $.body.data + format: json + message_deduplication_id: message_deduplication_id + http_headers: + key: http_headers + access_key_secret: access_key_secret + message_group_id: message_group_id + region: region + properties: + fleets: + items: + type: string + type: array + filter: + $ref: '#/components/schemas/Filter' + transform: + $ref: '#/components/schemas/RouteTransformSettings' + throttle_ms: + type: integer + url: + format: uri + type: string + http_headers: + additionalProperties: + type: string + type: object + disable_http_headers: + type: boolean + timeout: + type: integer + region: + type: string + access_key_id: + type: string + access_key_secret: + type: string + message_group_id: + type: string + message_deduplication_id: + type: string + channel: + type: string + type: object + RadRoute: + additionalProperties: false + example: + throttle_ms: 4 + event_id: 2 + test_api: true + fleets: + - fleets + - fleets + client_secret: client_secret + client_id: client_id + data_feed_key: data_feed_key + properties: + fleets: + items: + type: string + type: array + test_api: + type: boolean + data_feed_key: + type: string + event_id: + type: integer + client_id: + type: string + client_secret: + format: password + type: string + throttle_ms: + type: integer + type: object + AzureRoute: + additionalProperties: false + example: + filter: + files: + - env.qo + - motion.qo + type: inclusion + system_notefiles: false + transform: + jsonata_in: $.body.data + jsonata: $.body.data + format: json + throttle_ms: 7 + fleets: + - fleets + - fleets + sas_policy_name: sas_policy_name + functions_key_secret: functions_key_secret + url: https://openapi-generator.tech + timeout: 1 + sas_policy_key: sas_policy_key + properties: + fleets: + items: + type: string + type: array + filter: + $ref: '#/components/schemas/Filter' + transform: + $ref: '#/components/schemas/RouteTransformSettings' + throttle_ms: + type: integer + url: + format: uri + type: string + timeout: + type: integer + functions_key_secret: + type: string + sas_policy_name: + type: string + sas_policy_key: + type: string + type: object + ThingworxRoute: + additionalProperties: false + example: + filter: + files: + - env.qo + - motion.qo + type: inclusion + system_notefiles: false + transform: + jsonata_in: $.body.data + jsonata: $.body.data + format: json + throttle_ms: 1 + app_key: app_key + fleets: + - fleets + - fleets + url: https://openapi-generator.tech + timeout: 1 + properties: + fleets: + items: + type: string + type: array + filter: + $ref: '#/components/schemas/Filter' + transform: + $ref: '#/components/schemas/RouteTransformSettings' + throttle_ms: + type: integer + url: + format: uri + type: string + timeout: + type: integer + app_key: + type: string + type: object + SnowflakeRoute: + additionalProperties: false + example: + filter: + files: + - env.qo + - motion.qo + type: inclusion + system_notefiles: false + transform: + jsonata_in: $.body.data + jsonata: $.body.data + format: json + user_name: user_name + account_name: account_name + private_key_name: private_key_name + pem: pem + fleets: + - fleets + - fleets + organization_name: organization_name + timeout: 6 + properties: + fleets: + items: + type: string + type: array + filter: + $ref: '#/components/schemas/Filter' + transform: + $ref: '#/components/schemas/RouteTransformSettings' + timeout: + type: integer + organization_name: + type: string + account_name: + type: string + user_name: + type: string + private_key_name: + type: string + pem: + type: string + type: object + SnowpipeStreamingRoute: + additionalProperties: false + example: + filter: + files: + - env.qo + - motion.qo + type: inclusion + system_notefiles: false + pipe_name: pipe_name + transform: + jsonata_in: $.body.data + jsonata: $.body.data + format: json + database_name: database_name + user_name: user_name + account_name: account_name + private_key_name: private_key_name + pem: pem + fleets: + - fleets + - fleets + organization_name: organization_name + schema_name: schema_name + timeout: 7 + properties: + fleets: + items: + type: string + type: array + filter: + $ref: '#/components/schemas/Filter' + transform: + $ref: '#/components/schemas/RouteTransformSettings' + timeout: + type: integer + organization_name: + type: string + account_name: + type: string + database_name: + type: string + schema_name: + type: string + pipe_name: + type: string + user_name: + type: string + private_key_name: + type: string + pem: + type: string + type: object + TwilioRoute: + additionalProperties: false + example: + filter: + files: + - env.qo + - motion.qo + type: inclusion + system_notefiles: false + throttle_ms: 4 + account_sid: account_sid + fleets: + - fleets + - fleets + from: from + to: to + auth_token: auth_token + message: message + timeout: 1 + properties: + fleets: + items: + type: string + type: array + filter: + $ref: '#/components/schemas/Filter' + timeout: + type: integer + account_sid: + type: string + auth_token: + format: password + type: string + from: + type: string + to: + type: string + message: + type: string + throttle_ms: + type: integer + type: object + SlackRoute: + additionalProperties: false + example: + filter: + files: + - env.qo + - motion.qo + type: inclusion + system_notefiles: false + transform: + jsonata_in: $.body.data + jsonata: $.body.data + format: json + throttle_ms: 5 + webhook_url: https://openapi-generator.tech + blocks: blocks + channel: channel + bearer: bearer + fleets: + - fleets + - fleets + text: text + timeout: 9 + properties: + fleets: + items: + type: string + type: array + filter: + $ref: '#/components/schemas/Filter' + transform: + $ref: '#/components/schemas/RouteTransformSettings' + throttle_ms: + type: integer + timeout: + type: integer + channel: + type: string + bearer: + type: string + text: + type: string + blocks: + type: string + webhook_url: + format: uri + type: string + type: object + S3ArchiveRoute: + additionalProperties: false + example: + throttle_ms: 9 + key_id: key_id + bucket_name: bucket_name + fleets: + - fleets + - fleets + file_access: file_access + timeout: 6 + url: https://openapi-generator.tech + filter: + files: + - env.qo + - motion.qo + type: inclusion + system_notefiles: false + archive_every_mins: 9 + file_folder: file_folder + transform: + jsonata_in: $.body.data + jsonata: $.body.data + format: json + bucket_region: bucket_region + archive_count_exceeds: 8 + key_secret: key_secret + archive_id: archive_id + file_format: file_format + bucket_endpoint: bucket_endpoint + properties: + fleets: + items: + type: string + type: array + filter: + $ref: '#/components/schemas/Filter' + transform: + $ref: '#/components/schemas/RouteTransformSettings' + throttle_ms: + type: integer + timeout: + type: integer + url: + format: uri + type: string + archive_id: + type: string + archive_count_exceeds: + type: integer + archive_every_mins: + type: integer + file_access: + type: string + file_folder: + type: string + file_format: + type: string + bucket_endpoint: + type: string + bucket_region: + type: string + bucket_name: + type: string + key_id: + type: string + key_secret: + type: string + type: object + DatacakeRoute: + additionalProperties: false + example: + filter: + files: + - env.qo + - motion.qo + type: inclusion + system_notefiles: false + transform: + jsonata_in: $.body.data + jsonata: $.body.data + format: json + throttle_ms: 6 + http_headers: + key: http_headers + disable_http_headers: true + fleets: + - fleets + - fleets + url: https://openapi-generator.tech + timeout: 3 + properties: + fleets: + items: + type: string + type: array + filter: + $ref: '#/components/schemas/Filter' + transform: + $ref: '#/components/schemas/RouteTransformSettings' + throttle_ms: + type: integer + url: + format: uri + type: string + http_headers: + additionalProperties: + type: string + type: object + disable_http_headers: + type: boolean + timeout: + type: integer + type: object + BlynkRoute: + additionalProperties: false + example: + filter: + files: + - env.qo + - motion.qo + type: inclusion + system_notefiles: false + transform: + jsonata_in: $.body.data + jsonata: $.body.data + format: json + throttle_ms: 6 + fleets: + - fleets + - fleets + region: region + timeout: 1 + properties: + fleets: + items: + type: string + type: array + filter: + $ref: '#/components/schemas/Filter' + transform: + $ref: '#/components/schemas/RouteTransformSettings' + throttle_ms: + type: integer + timeout: + type: integer + region: + type: string + type: object + QubitroRoute: + additionalProperties: false + example: + filter: + files: + - env.qo + - motion.qo + type: inclusion + system_notefiles: false + transform: + jsonata_in: $.body.data + jsonata: $.body.data + format: json + throttle_ms: 2 + webhook_signing_key: webhook_signing_key + project_id: project_id + fleets: + - fleets + - fleets + timeout: 6 + properties: + fleets: + items: + type: string + type: array + filter: + $ref: '#/components/schemas/Filter' + transform: + $ref: '#/components/schemas/RouteTransformSettings' + throttle_ms: + type: integer + timeout: + type: integer + webhook_signing_key: + type: string + project_id: + type: string + type: object + NotehubRouteSummary: + example: + uid: route:8d65a087d5d290ce5bdf03aeff2becc0 + modified: 2000-01-23T04:56:07.000+00:00 + disabled: false + label: success route + type: http + properties: + uid: + default: route:8d65a087d5d290ce5bdf03aeff2becc0 + type: string + label: + default: success route + type: string + type: + default: http + type: string + modified: + format: date-time + readOnly: true + type: string + disabled: + default: false + type: boolean + type: object + EnvironmentVariables: + example: + environment_variables: + key: environment_variables + properties: + environment_variables: + additionalProperties: + type: string + type: object + required: + - environment_variables + type: object + RouteLog: + example: + date: date + attn: true + duration: 0 + route_uid: route_uid + text: text + url: url + event_uid: event_uid + status: status + properties: + date: + description: The date of the logs. + type: string + route_uid: + description: The route UID. + type: string + event_uid: + description: The event UID. + type: string + attn: + description: "If true, an error was returned when routing" + type: boolean + status: + description: The status of the event. + type: string + text: + description: The response body of the route. + type: string + url: + description: The URL of the route. + type: string + duration: + description: The duration of the route in milliseconds + type: integer + type: object + Note: + example: + payload: payload + body: "{}" + properties: + body: + type: object + payload: + type: string + type: object + Body: + example: + body: "{}" + properties: + body: + type: object + type: object + Monitor: + example: + silenced: true + aggregate_function: none + last_routed_at: last_routed_at + condition_type: greater_than + description: description + source_type: event + threshold: 0 + source_selector: body.temperature + routing_cooldown_period: 10m or 5h30m40s + uid: uid + fleet_filter: + - fleet_filter + - fleet_filter + aggregate_window: 10m or 5h30m40s + alert: true + name: name + disabled: true + per_device: true + notefile_filter: + - notefile_filter + - notefile_filter + alert_routes: + - message_type: text + text: text + url: url + - message_type: text + text: text + url: url + properties: + uid: + type: string + name: + type: string + description: + type: string + source_type: + description: The type of source to monitor. Currently only "event" is supported. + enum: + - event + type: string + disabled: + description: "If true, the monitor will not be evaluated." + type: boolean + alert: + description: "If true, the monitor is in alert state." + type: boolean + notefile_filter: + items: + type: string + type: array + fleet_filter: + items: + type: string + type: array + source_selector: + description: "A valid JSONata expression that selects the value to monitor\ + \ from the source. | It should return a single, numeric value." + example: body.temperature + type: string + condition_type: + description: "A comparison operation to apply to the value selected by the\ + \ source_selector [greater_than, greater_than_or_equal_to, less_than,\ + \ less_than_or_equal_to, equal_to, not_equal_to]" + enum: + - greater_than + - greater_than_or_equal_to + - less_than + - less_than_or_equal_to + - equal_to + - not_equal_to + type: string + threshold: + description: The type of condition to apply to the value selected by the + source_selector + type: integer + alert_routes: + items: + $ref: '#/components/schemas/Monitor_alert_routes_inner' + type: array + last_routed_at: + description: The last time the monitor was evaluated and routed. + type: string + silenced: + description: "If true, alerts will be created, but no notifications will\ + \ be sent." + type: boolean + routing_cooldown_period: + description: The time period to wait before routing another event after + the monitor | has been triggered. It follows the format of a number followed + by a time unit. + example: 10m or 5h30m40s + pattern: "^[0-9]+[smh]$" + type: string + aggregate_function: + description: "Aggregate function to apply to the selected values before\ + \ applying the condition. [none, sum, average, max, min]" + enum: + - none + - sum + - average + - max + - min + type: string + aggregate_window: + description: The time window to aggregate the selected values. It follows + the format of a number followed by a time unit + example: 10m or 5h30m40s + pattern: "^[0-9]+[smh]$" + type: string + per_device: + description: "Only relevant when using an aggregate_function. If true, the\ + \ monitor will be evaluated per device, | rather than across the set of\ + \ selected devices. If true then if a single device matches the specified\ + \ criteria, | and alert will be created, otherwise the aggregate function\ + \ will be applied across all devices." + type: boolean + type: object + CreateMonitor: + allOf: + - $ref: '#/components/schemas/Monitor' + - required: + - alert_routes + - description + - name + - notefile_filter + example: + silenced: true + aggregate_function: none + last_routed_at: last_routed_at + condition_type: greater_than + description: description + source_type: event + threshold: 0 + source_selector: body.temperature + routing_cooldown_period: 10m or 5h30m40s + uid: uid + fleet_filter: + - fleet_filter + - fleet_filter + aggregate_window: 10m or 5h30m40s + alert: true + name: name + disabled: true + per_device: true + notefile_filter: + - notefile_filter + - notefile_filter + alert_routes: + - message_type: text + text: text + url: url + - message_type: text + text: text + url: url + Empty: + type: object + FirmwareInfo: + example: + product: product + filename: filename + built: built + created: created + organization: organization + description: description + published: true + type: type + version: version + md5: md5 + tags: tags + target: target + properties: + filename: + description: The name of the firmware file. + type: string + version: + description: The version of the firmware. + type: string + md5: + description: The MD5 hash of the firmware file. + type: string + organization: + description: The organization that owns the firmware. + type: string + built: + description: The date the firmware was built. + type: string + product: + description: The product that the firmware is for. + type: string + description: + description: A description of the firmware. + type: string + tags: + description: A list of tags associated with the firmware. + type: string + type: + description: The type of firmware. + type: string + created: + description: The date the firmware was created. + type: string + target: + description: The target device for the firmware. + type: string + published: + description: True if the firmware is published. + type: boolean + type: object + Alert: + example: + monitor_uid: monitor_uid + data: + - alert_source: app + source_uid: source_uid + source_type: event + source: source + value: 5.962133916683182 + when: when + - alert_source: app + source_uid: source_uid + source_type: event + source: source + value: 5.962133916683182 + when: when + created_at: 0 + source: source + version: 1 + monitor_name: monitor_name + field_name: field_name + uid: uid + device_uid: device_uid + alert_source: app + value: 6.027456183070403 + monitor_type: event + notifications: + - notification_type: email + recipients: recipients + status: 5.637376656633329 + - notification_type: email + recipients: recipients + status: 5.637376656633329 + resolved: true + properties: + uid: + description: Alert UID + type: string + monitor_uid: + description: Monitor UID + type: string + monitor_name: + description: Monitor Name + type: string + device_uid: + description: Device UID + type: string + created_at: + description: The time the alert was created + type: integer + value: + description: The value that triggered the alert + type: number + resolved: + description: "If true, the alert has been resolved" + type: boolean + version: + description: The version of the alert + type: integer + alert_source: + description: The source of the alert + enum: + - app + - device + type: string + source: + description: The UID of the source of the alert + type: string + monitor_type: + description: The type of monitor that triggered the alert + enum: + - event + - device + type: string + field_name: + description: The field name that triggered the alert + type: string + data: + items: + $ref: '#/components/schemas/Alert_data_inner' + type: array + notifications: + items: + $ref: '#/components/schemas/Alert_notifications_inner' + type: array + type: object + UserFirmwareInfo: + properties: + current_firmware: + $ref: '#/components/schemas/CurrentFirmware' + firmware_update: + $ref: '#/components/schemas/UserDfuStateMachine' + type: object + UserDfuStateMachine: + properties: + status: + $ref: '#/components/schemas/UserDfuStateMachineStatus' + created: + format: date-time + nullable: true + type: string + from_version: + type: string + metadata: + $ref: '#/components/schemas/UploadMetadata' + type: object + UserDfuStateMachineStatus: + properties: + phase: + type: string + phase_description: + type: string + date: + format: date-time + nullable: true + type: string + status: + type: string + type: object + CurrentFirmware: + properties: + version: + type: string + metadata: + $ref: '#/components/schemas/Firmware' + type: object + UploadMetadata: + properties: + name: + type: string + length: + type: integer + md5: + type: string + crc32: + type: integer + created: + type: integer + modified: + type: integer + source: + type: string + contains: + type: string + found: + type: string + type: + type: string + tags: + type: string + notes: + type: string + firmware: + $ref: '#/components/schemas/Firmware' + version: + description: User-specified version string provided at time of upload + type: string + type: object + Firmware: + nullable: true + properties: + org: + type: string + product: + type: string + firmware: + type: string + version: + type: string + target: + type: string + ver_major: + type: integer + ver_minor: + type: integer + ver_patch: + type: integer + ver_build: + type: integer + built: + type: string + builder: + type: string + type: object + SlackBearerNotification: + properties: + token: + description: The bearer token for the Slack app. + type: string + channel: + description: The channel to send the message to. + type: string + message_type: + description: text or blocks + enum: + - text + - blocks + type: string + text: + description: "The text of the message, or the blocks definition" + type: string + type: object + SlackWebHookNotification: + example: + message_type: text + text: text + url: url + properties: + url: + description: The URL of the Slack webhook. + type: string + message_type: + description: text or blocks + enum: + - text + - blocks + type: string + text: + description: "The text of the message, or the blocks definition" + type: string + type: object + EmailNotification: + properties: + email: + description: Email Address + example: example@blues.com + type: string + type: object + DfuActionRequest: + example: + filename: filename + properties: + filename: + description: The name of the firmware file + type: string + type: object + DeviceDfuStatus: + example: + current: + product: product + built: built + organization: organization + builder: builder + description: description + version: version + device_uid: device_uid + dfu_in_progress: true + status: + initiated: initiated + requested_version: requested_version + current_version: current_version + updates: + - phase: phase + datetime: datetime + description: description + status: status + - phase: phase + datetime: datetime + description: description + status: status + properties: + device_uid: + description: Device UID + type: string + dfu_in_progress: + description: true if there is a DFU currently in progress + type: boolean + current: + $ref: '#/components/schemas/DeviceDfuStatus_current' + status: + $ref: '#/components/schemas/DeviceDfuStateMachine' + type: object + DeviceDfuStatusPage: + example: + devices: + - current: + product: product + built: built + organization: organization + builder: builder + description: description + version: version + device_uid: device_uid + dfu_in_progress: true + status: + initiated: initiated + requested_version: requested_version + current_version: current_version + updates: + - phase: phase + datetime: datetime + description: description + status: status + - phase: phase + datetime: datetime + description: description + status: status + - current: + product: product + built: built + organization: organization + builder: builder + description: description + version: version + device_uid: device_uid + dfu_in_progress: true + status: + initiated: initiated + requested_version: requested_version + current_version: current_version + updates: + - phase: phase + datetime: datetime + description: description + status: status + - phase: phase + datetime: datetime + description: description + status: status + has_more: false + properties: + devices: + items: + $ref: '#/components/schemas/DeviceDfuStatus' + type: array + has_more: + default: false + type: boolean + type: object + DeviceDfuHistory: + example: + current: + product: product + built: built + organization: organization + builder: builder + description: description + version: version + device_uid: device_uid + history: + - initiated: initiated + requested_version: requested_version + current_version: current_version + updates: + - phase: phase + datetime: datetime + description: description + status: status + - phase: phase + datetime: datetime + description: description + status: status + - initiated: initiated + requested_version: requested_version + current_version: current_version + updates: + - phase: phase + datetime: datetime + description: description + status: status + - phase: phase + datetime: datetime + description: description + status: status + properties: + device_uid: + description: Device UID + type: string + current: + $ref: '#/components/schemas/DeviceDfuStatus_current' + history: + items: + $ref: '#/components/schemas/DeviceDfuStateMachine' + type: array + type: object + DeviceDfuHistoryPage: + example: + devices: + - current: + product: product + built: built + organization: organization + builder: builder + description: description + version: version + device_uid: device_uid + history: + - initiated: initiated + requested_version: requested_version + current_version: current_version + updates: + - phase: phase + datetime: datetime + description: description + status: status + - phase: phase + datetime: datetime + description: description + status: status + - initiated: initiated + requested_version: requested_version + current_version: current_version + updates: + - phase: phase + datetime: datetime + description: description + status: status + - phase: phase + datetime: datetime + description: description + status: status + - current: + product: product + built: built + organization: organization + builder: builder + description: description + version: version + device_uid: device_uid + history: + - initiated: initiated + requested_version: requested_version + current_version: current_version + updates: + - phase: phase + datetime: datetime + description: description + status: status + - phase: phase + datetime: datetime + description: description + status: status + - initiated: initiated + requested_version: requested_version + current_version: current_version + updates: + - phase: phase + datetime: datetime + description: description + status: status + - phase: phase + datetime: datetime + description: description + status: status + has_more: false + properties: + devices: + items: + $ref: '#/components/schemas/DeviceDfuHistory' + type: array + has_more: + default: false + type: boolean + type: object + DeviceDfuStateMachine: + description: Represents a single request to update the host or Notecard firmware + example: + initiated: initiated + requested_version: requested_version + current_version: current_version + updates: + - phase: phase + datetime: datetime + description: description + status: status + - phase: phase + datetime: datetime + description: description + status: status + properties: + requested_version: + description: Version of the firmware that was requested to be installed + type: string + current_version: + description: Version of the firmware that was installed prior to this request + type: string + initiated: + description: RFC3339 datetime of when this update was requested + type: string + updates: + items: + $ref: '#/components/schemas/DeviceDfuStateMachineNode' + type: array + type: object + DeviceDfuStateMachineNode: + description: Represents a single request to update the host or Notecard firmware + example: + phase: phase + datetime: datetime + description: description + status: status + properties: + status: + description: Status for this step in the firmware update process + type: string + phase: + description: Phase for this step in the firmware update process + type: string + datetime: + description: RFC3339 compatible datetime of when this status update happened + type: string + description: + description: Additional information + type: string + type: object + DataField: + properties: + name: + description: The name of the field + type: string + type: object + DataSetField: + example: + datatype: 0 + jsonata: jsonata + name: name + properties: + name: + description: The name of the field + type: string + datatype: + description: The datatype of the field + enum: + - 0 + - 1 + - 2 + type: integer + jsonata: + description: the JSONata expression used to populate this field + type: string + type: object + DataSet: + example: + name: name + lon: lon + time: time + fields: + - datatype: 0 + jsonata: jsonata + name: name + - datatype: 0 + jsonata: jsonata + name: name + lat: lat + properties: + name: + description: The name of the data set + type: string + time: + description: JSONata expression resulting in the relevant time field + type: string + lat: + description: JSONata expression resulting in the latitude field + type: string + lon: + description: JSONata expression resulting in the Longitude field + type: string + fields: + items: + $ref: '#/components/schemas/DataSetField' + type: array + FleetsUIDList: + items: + type: string + type: array + AppUIDList: + items: + type: string + type: array + CreateUpdateRepository: + example: + fleet_uids: + - fleet_uids + - fleet_uids + name: name + project_uids: + - project_uids + - project_uids + properties: + name: + type: string + fleet_uids: + items: + type: string + type: array + project_uids: + items: + type: string + type: array + type: object + Repository: + example: + uid: uid + fleet_uids: + - fleet_uids + - fleet_uids + name: name + project_uids: + - project_uids + - project_uids + properties: + uid: + description: The unique identifier for the data repository + type: string + name: + description: repository name + type: string + fleet_uids: + items: + type: string + type: array + project_uids: + items: + type: string + type: array + type: object + NotefileSchema: + example: + notefile: notefile + properties: + - updated_at: 2000-01-23T04:56:07.000+00:00 + name: name + type: string + items: + - null + - null + properties: + - null + - null + - updated_at: 2000-01-23T04:56:07.000+00:00 + name: name + type: string + items: + - null + - null + properties: + - null + - null + properties: + notefile: + type: string + properties: + items: + $ref: '#/components/schemas/SchemaProperty' + type: array + required: + - notefile + - properties + type: object + SchemaProperty: + example: + updated_at: 2000-01-23T04:56:07.000+00:00 + name: name + type: string + items: + - null + - null + properties: + - null + - null + properties: + name: + description: Name of the field (optional for array/object children) + type: string + type: + enum: + - string + - number + - boolean + - array + - object + type: string + updated_at: + format: date-time + type: string + items: + description: Used if type is array + items: + $ref: '#/components/schemas/SchemaProperty' + type: array + properties: + description: Used if type is object + items: + $ref: '#/components/schemas/SchemaProperty' + type: array + required: + - type + - updatedAt + type: object + CellularUsage: + items: + $ref: '#/components/schemas/SimUsage' + type: array + SimUsage: + example: + iccid: iccid + last_updated: 4 + limit: 7 + used: 6 + lifetime_used: 1 + properties: + iccid: + description: ICCID of the SIM card + type: string + used: + description: Bytes used on the SIMs current data plan + format: int64 + type: integer + limit: + description: Limit in bytes of the SIMs current data plan + format: int64 + type: integer + lifetime_used: + description: Total number of bytes used by SIM + format: int64 + type: integer + last_updated: + description: Time this usage information was last updated + format: int64 + type: integer + type: object + WebhookSettings: + example: + transform: transform + disabled: true + id: id + properties: + disabled: + description: Flag indicating if the webhook is disabled + type: boolean + id: + description: Webhook ID + type: string + transform: + description: Transformation to be applied to the event + type: string + type: object + EnvTreeJsonNode: + example: + var_count: 0 + app_uid: app_uid + app_label: app_label + inherited_var_count: 6 + variables: + - used: true + value: value + key: key + precedence: 1 + - used: true + value: value + key: key + precedence: 1 + device_uid: device_uid + fleet_label: fleet_label + children: + - null + - null + type: type + fleet_uid: fleet_uid + url: https://openapi-generator.tech + properties: + var_count: + type: integer + inherited_var_count: + type: integer + type: + type: string + variables: + items: + $ref: '#/components/schemas/EnvVar' + type: array + children: + items: + $ref: '#/components/schemas/EnvTreeJsonNode' + type: array + device_uid: + type: string + fleet_label: + type: string + fleet_uid: + type: string + app_uid: + type: string + app_label: + type: string + url: + format: uri + type: string + required: + - children + - inherited_var_count + - type + - var_count + - variables + type: object + EnvVar: + example: + used: true + value: value + key: key + precedence: 1 + properties: + key: + type: string + value: + type: string + used: + type: boolean + precedence: + type: integer + type: object + UsageData: + example: + period: 2025-07-23T00:00:00Z + total_bytes: 1048576 + bytes_received: 524288 + bytes_sent: 524288 + properties: + period: + example: 2025-07-23T00:00:00Z + format: date-time + type: string + total_bytes: + example: 1048576 + type: integer + bytes_received: + example: 524288 + type: integer + bytes_sent: + example: 524288 + type: integer + required: + - period + - total_bytes + type: object + CellularPlan: + example: + iccid: "345678432765434567890765746354465786" + plan_type: "500MB, North America, 10-year lifetime" + last_updated: 1656010061 + expires_at: 6 + data_usage: + kb_used: 127.45 + kb_remaining: 372.55 + kb_total: 500 + imsi: "310170830688975" + activated: 1656010061 + lifetime_used: 0 + properties: + iccid: + description: The Integrated Circuit Card Identifier of the SIM card + example: "345678432765434567890765746354465786" + type: string + imsi: + description: IMSI of the SIM card + example: "310170830688975" + type: string + plan_type: + description: "Description of the SIM plan type including data allowance,\ + \ region, and validity period" + example: "500MB, North America, 10-year lifetime" + type: string + data_usage: + $ref: '#/components/schemas/DataUsage' + last_updated: + description: Time this plan information was last updated + example: 1656010061 + format: int64 + type: integer + activated: + description: Unix timestamp of when the SIM was activated + example: 1656010061 + format: int64 + type: integer + lifetime_used: + description: Total bytes used by this SIM + format: int64 + type: integer + expires_at: + format: int64 + type: integer + type: object + SatellitePlan: + nullable: true + properties: + ntn_provider: + description: Non-Terrestrial Network provider name + example: Skylo + type: string + psid: + description: Provider-specific identifier for the satellite subscription + example: skylo:5746354465786 + type: string + activated: + description: Activation date of the satellite plan as Unix timestamp + example: 1609459200 + format: int64 + type: integer + billable_bytes: + $ref: '#/components/schemas/DataUsage' + last_updated: + description: Time this plan information was last updated + example: 1656010061 + format: int64 + type: integer + required: + - activated + - cumulative_billable_bytes + - ntn_provider + - psid + type: object + DataUsage: + example: + kb_used: 127.45 + kb_remaining: 372.55 + kb_total: 500 + properties: + kb_total: + description: Total Kilobytes included in the plan + example: 500 + format: double + type: number + kb_used: + description: Kilobytes used to date + example: 127.45 + format: double + type: number + kb_remaining: + description: Kilobytes remaining in the plan + example: 372.55 + format: double + type: number + required: + - kb_remaining + - kb_total + - kb_used + type: object + Login_request: + properties: + username: + type: string + password: + type: string + type: object + Login_200_response: + example: + session_token: session_token + properties: + session_token: + type: string + type: object + OAuth2ClientCredentials_request: + properties: + grant_type: + enum: + - client_credentials + example: client_credentials + type: string + client_id: + example: d37ae69a-c739-44e9-9722-81a4cb7aeb67 + format: uuid + type: string + client_secret: + example: 2a40832d028a3e24a717f023fdcaa8ab452dd12223d472aff38223ffdfeee4bf + format: password + type: string + scope: + description: Space-delimited scopes. + example: openid + type: string + required: + - client_id + - client_secret + - grant_type + type: object + GetBillingAccounts_200_response: + example: + billing_accounts: + - uid: uid + role: billing_admin + name: name + - uid: uid + role: billing_admin + name: name + properties: + billing_accounts: + items: + $ref: '#/components/schemas/BillingAccount' + type: array + type: object + GetProjects_200_response: + example: + projects: + - uid: uid + role: owner + technical_contact: + role: role + organization: organization + name: name + email: email + administrative_contact: + role: role + organization: organization + name: name + email: email + created: 2000-01-23T04:56:07.000+00:00 + label: label + - uid: uid + role: owner + technical_contact: + role: role + organization: organization + name: name + email: email + administrative_contact: + role: role + organization: organization + name: name + email: email + created: 2000-01-23T04:56:07.000+00:00 + label: label + properties: + projects: + items: + $ref: '#/components/schemas/Project' + type: array + type: object + CreateProject_request: + properties: + label: + description: The label for the project. + type: string + billing_account_uid: + description: | + The billing account UID for the project. The caller of the API must be able to create projects within the billing account, otherwise an error will be returned. + type: string + required: + - billing_account_uid + - label + type: object + CloneProject_request: + properties: + label: + description: The label for the project. + type: string + billing_account_uid: + description: "The billing account UID for the project. The caller of the\ + \ API must be able to create projects within the billing account, otherwise\ + \ an error will be returned." + type: string + disable_clone_routes: + description: Whether to disallow the cloning of the routes from the parent + project. Default is false if not specified. + type: boolean + disable_clone_fleets: + description: Whether to disallow the cloning of the fleets from the parent + project. Default is false if not specified. + type: boolean + required: + - billing_account_uid + - label + type: object + GetProducts_200_response: + example: + products: + - uid: uid + disable_devices_by_default: true + auto_provision_fleets: + - auto_provision_fleets + - auto_provision_fleets + label: label + - uid: uid + disable_devices_by_default: true + auto_provision_fleets: + - auto_provision_fleets + - auto_provision_fleets + label: label + properties: + products: + items: + $ref: '#/components/schemas/Product' + type: array + type: object + CreateProduct_request: + properties: + product_uid: + description: The requested uid for the Product. Will be prefixed with the + user's reversed email. + type: string + label: + description: The label for the Product. + type: string + auto_provision_fleets: + items: + type: string + type: array + disable_devices_by_default: + description: "If `true`, devices provisioned to this product will be automatically\ + \ disabled by default." + type: boolean + required: + - label + - product_uid + type: object + GetFleets_200_response: + example: + fleets: + - uid: uid + environment_variables: + key: environment_variables + watchdog_mins: 0 + created: 2000-01-23T04:56:07.000+00:00 + connectivity_assurance: + enabled: true + smart_rule: smart_rule + label: label + - uid: uid + environment_variables: + key: environment_variables + watchdog_mins: 0 + created: 2000-01-23T04:56:07.000+00:00 + connectivity_assurance: + enabled: true + smart_rule: smart_rule + label: label + properties: + fleets: + items: + $ref: '#/components/schemas/Fleet' + type: array + required: + - fleets + type: object + CreateFleet_request: + properties: + label: + description: "The label, or name, for the Fleet." + type: string + smart_rule: + description: "JSONata expression that will be evaluated to determine device\ + \ membership into this fleet, if the expression evaluates to a 1, the\ + \ device will be included, if it evaluates to -1 it will be removed, and\ + \ if it evaluates to 0 or errors it will be left unchanged." + type: string + connectivity_assurance: + $ref: '#/components/schemas/FleetConnectivityAssurance' + type: object + UpdateFleet_request: + properties: + label: + description: The label for the Fleet. + type: string + addDevices: + description: List of DeviceUIDs to add to fleet + items: + type: string + type: array + removeDevices: + description: List of DeviceUIDs to remove from fleet + items: + type: string + type: array + smart_rule: + description: "JSONata expression that will be evaluated to determine device\ + \ membership into this fleet, if the expression evaluates to a 1, the\ + \ device will be included, if it evaluates to -1 it will be removed, and\ + \ if it evaluates to 0 or errors it will be left unchanged." + type: string + connectivity_assurance: + $ref: '#/components/schemas/FleetConnectivityAssurance' + watchdog_mins: + description: A watchdog timer is used to generate an event every N minutes + of inactivity. 0 means no watchdog + format: int64 + type: integer + type: object + GetProjectMembers_200_response: + example: + members: + - role: owner + name: name + email: email + - role: owner + name: name + email: email + properties: + members: + items: + $ref: '#/components/schemas/ProjectMember' + type: array + required: + - members + type: object + GetDevices_200_response: + example: + devices: + - cellular_usage: + - iccid: iccid + last_updated: 4 + limit: 7 + used: 6 + lifetime_used: 1 + - iccid: iccid + last_updated: 4 + limit: 7 + used: 6 + lifetime_used: 1 + fleet_uids: + - fleet_uids + - fleet_uids + serial_number: serial_number + tower_info: + mnc: 6 + mcc: 0 + cell_id: 5 + lac: 1 + tower_location: + country: country + timezone: timezone + latitude: 5.637376656633329 + name: name + when: when + longitude: 2.3021358869347655 + triangulated_location: + country: country + timezone: timezone + latitude: 5.637376656633329 + name: name + when: when + longitude: 2.3021358869347655 + voltage: 7.061401241503109 + gps_location: + country: country + timezone: timezone + latitude: 5.637376656633329 + name: name + when: when + longitude: 2.3021358869347655 + uid: uid + dfu: + user: + read: 1.0246457001441578 + began: 4.145608029883936 + length: 3.616076749251911 + type: card + version: version + mode: idle + file: file + crc32: 2.027123023002322 + updated: 1.4894159098541704 + retry: 7.386281948385884 + errors: 1.2315135367772556 + md5: md5 + status: status + card: + read: 1.0246457001441578 + began: 4.145608029883936 + length: 3.616076749251911 + type: card + version: version + mode: idle + file: file + crc32: 2.027123023002322 + updated: 1.4894159098541704 + retry: 7.386281948385884 + errors: 1.2315135367772556 + md5: md5 + status: status + last_activity: 2000-01-23T04:56:07.000+00:00 + provisioned: 2000-01-23T04:56:07.000+00:00 + contact: + role: role + organization: organization + name: name + email: email + firmware_host: firmware_host + temperature: 9.301444243932576 + disabled: true + product_uid: product_uid + sku: sku + firmware_notecard: firmware_notecard + - cellular_usage: + - iccid: iccid + last_updated: 4 + limit: 7 + used: 6 + lifetime_used: 1 + - iccid: iccid + last_updated: 4 + limit: 7 + used: 6 + lifetime_used: 1 + fleet_uids: + - fleet_uids + - fleet_uids + serial_number: serial_number + tower_info: + mnc: 6 + mcc: 0 + cell_id: 5 + lac: 1 + tower_location: + country: country + timezone: timezone + latitude: 5.637376656633329 + name: name + when: when + longitude: 2.3021358869347655 + triangulated_location: + country: country + timezone: timezone + latitude: 5.637376656633329 + name: name + when: when + longitude: 2.3021358869347655 + voltage: 7.061401241503109 + gps_location: + country: country + timezone: timezone + latitude: 5.637376656633329 + name: name + when: when + longitude: 2.3021358869347655 + uid: uid + dfu: + user: + read: 1.0246457001441578 + began: 4.145608029883936 + length: 3.616076749251911 + type: card + version: version + mode: idle + file: file + crc32: 2.027123023002322 + updated: 1.4894159098541704 + retry: 7.386281948385884 + errors: 1.2315135367772556 + md5: md5 + status: status + card: + read: 1.0246457001441578 + began: 4.145608029883936 + length: 3.616076749251911 + type: card + version: version + mode: idle + file: file + crc32: 2.027123023002322 + updated: 1.4894159098541704 + retry: 7.386281948385884 + errors: 1.2315135367772556 + md5: md5 + status: status + last_activity: 2000-01-23T04:56:07.000+00:00 + provisioned: 2000-01-23T04:56:07.000+00:00 + contact: + role: role + organization: organization + name: name + email: email + firmware_host: firmware_host + temperature: 9.301444243932576 + disabled: true + product_uid: product_uid + sku: sku + firmware_notecard: firmware_notecard + has_more: true + properties: + devices: + items: + $ref: '#/components/schemas/Device' + type: array + has_more: + type: boolean + required: + - devices + - has_more + type: object + ProvisionDevice_request: + properties: + product_uid: + description: The ProductUID that the device should use. + type: string + device_sn: + description: The serial number to assign to the device. + type: string + fleet_uids: + description: The fleetUIDs to provision the device to. + items: + type: string + nullable: true + type: array + required: + - product_uid + type: object + GetDevicePublicKeys_200_response_device_public_keys_inner: + example: + uid: uid + key: key + properties: + uid: + type: string + key: + type: string + type: object + GetDevicePublicKeys_200_response: + example: + device_public_keys: + - uid: uid + key: key + - uid: uid + key: key + has_more: true + properties: + device_public_keys: + items: + $ref: '#/components/schemas/GetDevicePublicKeys_200_response_device_public_keys_inner' + type: array + has_more: + type: boolean + required: + - device_public_keys + - has_more + type: object + GetDevicePlans_200_response: + example: + cellular_plans: + - iccid: "345678432765434567890765746354465786" + plan_type: "500MB, North America, 10-year lifetime" + last_updated: 1656010061 + expires_at: 6 + data_usage: + kb_used: 127.45 + kb_remaining: 372.55 + kb_total: 500 + imsi: "310170830688975" + activated: 1656010061 + lifetime_used: 0 + - iccid: "345678432765434567890765746354465786" + plan_type: "500MB, North America, 10-year lifetime" + last_updated: 1656010061 + expires_at: 6 + data_usage: + kb_used: 127.45 + kb_remaining: 372.55 + kb_total: 500 + imsi: "310170830688975" + activated: 1656010061 + lifetime_used: 0 + properties: + cellular_plans: + items: + $ref: '#/components/schemas/CellularPlan' + nullable: true + type: array + type: object + AddDeviceToFleets_request: + properties: + fleet_uids: + description: The fleetUIDs to add to the device. + items: + type: string + type: array + required: + - fleet_uids + type: object + DeleteDeviceFromFleets_request: + properties: + fleet_uids: + description: The fleetUIDs to remove from the device. + items: + type: string + type: array + required: + - fleet_uids + type: object + GetDevicePublicKey_200_response: + example: + uid: uid + key: key + properties: + uid: + type: string + key: + type: string + required: + - key + - uid + type: object + GetDeviceLatestEvents_200_response: + example: + latest_events: + - moved: 7.457744773683766 + rsrp: 5.025004791520295 + where_when: 7.061401241503109 + body: "{}" + when: 6.027456183070403 + where_timezone: where_timezone + tri_country: tri_country + tri_points: 6.84685269835264 + where_lon: 3.616076749251911 + best_lat: 5.637376656633329 + best_timezone: best_timezone + payload: payload + where_country: where_country + tri_timezone: tri_timezone + tower_timezone: tower_timezone + sku: sku + tower_lat: 4.145608029883936 + req: req + app: app + tower_id: tower_id + where_lat: 9.301444243932576 + rat: rat + best_lon: 2.3021358869347655 + received: 0.8008281904610115 + where_olc: where_olc + tower_location: tower_location + tower_country: tower_country + sinr: 4.965218492984954 + device: device + note: note + tri_lat: 1.0246457001441578 + session: session + bssid: bssid + best_location: best_location + updates: 1.4658129805029452 + ssid: ssid + best_id: best_id + ordering_code: ordering_code + file: file + best_country: best_country + sn: sn + event: event + tower_when: 2.027123023002322 + best_location_when: 5.962133916683182 + product: product + where_location: where_location + tower_lon: 7.386281948385884 + orientation: orientation + rssi: 1.1730742509559433 + temp: 8.762042012749001 + tri_when: 1.2315135367772556 + tri_location: tri_location + transport: transport + best_location_type: best_location_type + bars: 9.369310271410669 + voltage: 6.683562403749608 + environment: "{}" + rsrq: 9.965781217890562 + tri_lon: 1.4894159098541704 + tls: true + - moved: 7.457744773683766 + rsrp: 5.025004791520295 + where_when: 7.061401241503109 + body: "{}" + when: 6.027456183070403 + where_timezone: where_timezone + tri_country: tri_country + tri_points: 6.84685269835264 + where_lon: 3.616076749251911 + best_lat: 5.637376656633329 + best_timezone: best_timezone + payload: payload + where_country: where_country + tri_timezone: tri_timezone + tower_timezone: tower_timezone + sku: sku + tower_lat: 4.145608029883936 + req: req + app: app + tower_id: tower_id + where_lat: 9.301444243932576 + rat: rat + best_lon: 2.3021358869347655 + received: 0.8008281904610115 + where_olc: where_olc + tower_location: tower_location + tower_country: tower_country + sinr: 4.965218492984954 + device: device + note: note + tri_lat: 1.0246457001441578 + session: session + bssid: bssid + best_location: best_location + updates: 1.4658129805029452 + ssid: ssid + best_id: best_id + ordering_code: ordering_code + file: file + best_country: best_country + sn: sn + event: event + tower_when: 2.027123023002322 + best_location_when: 5.962133916683182 + product: product + where_location: where_location + tower_lon: 7.386281948385884 + orientation: orientation + rssi: 1.1730742509559433 + temp: 8.762042012749001 + tri_when: 1.2315135367772556 + tri_location: tri_location + transport: transport + best_location_type: best_location_type + bars: 9.369310271410669 + voltage: 6.683562403749608 + environment: "{}" + rsrq: 9.965781217890562 + tri_lon: 1.4894159098541704 + tls: true + properties: + latest_events: + description: The set of latest events. Will always include the current + "session.begin" event. + items: + $ref: '#/components/schemas/Event' + type: array + type: object + GetDeviceHealthLog_200_response_health_log_inner: + example: + alert: true + text: text + when: 2000-01-23T04:56:07.000+00:00 + properties: + when: + format: date-time + type: string + alert: + type: boolean + text: + type: string + required: + - alert + - text + - when + type: object + GetDeviceHealthLog_200_response: + example: + health_log: + - alert: true + text: text + when: 2000-01-23T04:56:07.000+00:00 + - alert: true + text: text + when: 2000-01-23T04:56:07.000+00:00 + properties: + health_log: + items: + $ref: '#/components/schemas/GetDeviceHealthLog_200_response_health_log_inner' + type: array + required: + - health_log + type: object + GetDeviceSessions_200_response: + example: + sessions: + - moved: 9 + rsrp: 5 + where_when: 1 + when: 7 + where_timezone: where_timezone + where_lon: 5.025004791520295 + iccid: iccid + penalty_secs: 8 + where_country: where_country + continuous: true + bearer: bearer + hp_secs_total: 6 + apn: apn + events: 8 + tower: + c: c + mnc: 7 + lon: 2.027123023002322 + source: source + mcc: 4 + l: l + "n": "n" + lac: 1 + towers: 6 + zone: zone + z: 1 + time: 9 + lat: 3.616076749251911 + cid: 1 + triangulate: "{}" + power_primary: true + period: + duration: 5 + sessions_tls: 7 + notes_sent: 0 + bytes_rcvd: 6 + bytes_rcvd_secondary: 3 + sessions_tcp: 0 + notes_rcvd: 6 + bytes_sent_secondary: 7 + bytes_sent: 3 + since: 6 + rat: rat + where_lat: 4.965218492984954 + work: 6 + ip: ip + tri: + c: c + mnc: 7 + lon: 2.027123023002322 + source: source + mcc: 4 + l: l + "n": "n" + lac: 1 + towers: 6 + zone: zone + z: 1 + time: 9 + lat: 3.616076749251911 + cid: 1 + why_session_closed: why_session_closed + hp_secs_data: 3 + sinr: 5 + power_usb: true + power_charging: true + session_began: 0 + device: device + why_session_opened: why_session_opened + failed_connects: 7 + session: session + bssid: bssid + scan: scan + fleets: + - fleets + - fleets + cell: cell + ssid: ssid + usage_actual: true + hp_cycles_data: 2 + hp_secs_gps: 6 + where: where + sn: sn + product: product + rssi: 1 + where_location: where_location + temp: 9.369310271410669 + orientation: orientation + transport: transport + bars: 7 + voltage: 9.965781217890562 + hp_cycles_gps: 6 + rsrq: 2 + tls: true + hp_cycles_total: 1 + session_ended: 6 + power_mah: 4.652396432933246 + - moved: 9 + rsrp: 5 + where_when: 1 + when: 7 + where_timezone: where_timezone + where_lon: 5.025004791520295 + iccid: iccid + penalty_secs: 8 + where_country: where_country + continuous: true + bearer: bearer + hp_secs_total: 6 + apn: apn + events: 8 + tower: + c: c + mnc: 7 + lon: 2.027123023002322 + source: source + mcc: 4 + l: l + "n": "n" + lac: 1 + towers: 6 + zone: zone + z: 1 + time: 9 + lat: 3.616076749251911 + cid: 1 + triangulate: "{}" + power_primary: true + period: + duration: 5 + sessions_tls: 7 + notes_sent: 0 + bytes_rcvd: 6 + bytes_rcvd_secondary: 3 + sessions_tcp: 0 + notes_rcvd: 6 + bytes_sent_secondary: 7 + bytes_sent: 3 + since: 6 + rat: rat + where_lat: 4.965218492984954 + work: 6 + ip: ip + tri: + c: c + mnc: 7 + lon: 2.027123023002322 + source: source + mcc: 4 + l: l + "n": "n" + lac: 1 + towers: 6 + zone: zone + z: 1 + time: 9 + lat: 3.616076749251911 + cid: 1 + why_session_closed: why_session_closed + hp_secs_data: 3 + sinr: 5 + power_usb: true + power_charging: true + session_began: 0 + device: device + why_session_opened: why_session_opened + failed_connects: 7 + session: session + bssid: bssid + scan: scan + fleets: + - fleets + - fleets + cell: cell + ssid: ssid + usage_actual: true + hp_cycles_data: 2 + hp_secs_gps: 6 + where: where + sn: sn + product: product + rssi: 1 + where_location: where_location + temp: 9.369310271410669 + orientation: orientation + transport: transport + bars: 7 + voltage: 9.965781217890562 + hp_cycles_gps: 6 + rsrq: 2 + tls: true + hp_cycles_total: 1 + session_ended: 6 + power_mah: 4.652396432933246 + has_more: true + properties: + sessions: + items: + $ref: '#/components/schemas/DeviceSession' + type: array + has_more: + type: boolean + required: + - has_more + - sessions + type: object + GetEvents_200_response: + example: + through: through + has_more: true + events: + - moved: 7.457744773683766 + rsrp: 5.025004791520295 + where_when: 7.061401241503109 + body: "{}" + when: 6.027456183070403 + where_timezone: where_timezone + tri_country: tri_country + tri_points: 6.84685269835264 + where_lon: 3.616076749251911 + best_lat: 5.637376656633329 + best_timezone: best_timezone + payload: payload + where_country: where_country + tri_timezone: tri_timezone + tower_timezone: tower_timezone + sku: sku + tower_lat: 4.145608029883936 + req: req + app: app + tower_id: tower_id + where_lat: 9.301444243932576 + rat: rat + best_lon: 2.3021358869347655 + received: 0.8008281904610115 + where_olc: where_olc + tower_location: tower_location + tower_country: tower_country + sinr: 4.965218492984954 + device: device + note: note + tri_lat: 1.0246457001441578 + session: session + bssid: bssid + best_location: best_location + updates: 1.4658129805029452 + ssid: ssid + best_id: best_id + ordering_code: ordering_code + file: file + best_country: best_country + sn: sn + event: event + tower_when: 2.027123023002322 + best_location_when: 5.962133916683182 + product: product + where_location: where_location + tower_lon: 7.386281948385884 + orientation: orientation + rssi: 1.1730742509559433 + temp: 8.762042012749001 + tri_when: 1.2315135367772556 + tri_location: tri_location + transport: transport + best_location_type: best_location_type + bars: 9.369310271410669 + voltage: 6.683562403749608 + environment: "{}" + rsrq: 9.965781217890562 + tri_lon: 1.4894159098541704 + tls: true + - moved: 7.457744773683766 + rsrp: 5.025004791520295 + where_when: 7.061401241503109 + body: "{}" + when: 6.027456183070403 + where_timezone: where_timezone + tri_country: tri_country + tri_points: 6.84685269835264 + where_lon: 3.616076749251911 + best_lat: 5.637376656633329 + best_timezone: best_timezone + payload: payload + where_country: where_country + tri_timezone: tri_timezone + tower_timezone: tower_timezone + sku: sku + tower_lat: 4.145608029883936 + req: req + app: app + tower_id: tower_id + where_lat: 9.301444243932576 + rat: rat + best_lon: 2.3021358869347655 + received: 0.8008281904610115 + where_olc: where_olc + tower_location: tower_location + tower_country: tower_country + sinr: 4.965218492984954 + device: device + note: note + tri_lat: 1.0246457001441578 + session: session + bssid: bssid + best_location: best_location + updates: 1.4658129805029452 + ssid: ssid + best_id: best_id + ordering_code: ordering_code + file: file + best_country: best_country + sn: sn + event: event + tower_when: 2.027123023002322 + best_location_when: 5.962133916683182 + product: product + where_location: where_location + tower_lon: 7.386281948385884 + orientation: orientation + rssi: 1.1730742509559433 + temp: 8.762042012749001 + tri_when: 1.2315135367772556 + tri_location: tri_location + transport: transport + best_location_type: best_location_type + bars: 9.369310271410669 + voltage: 6.683562403749608 + environment: "{}" + rsrq: 9.965781217890562 + tri_lon: 1.4894159098541704 + tls: true + properties: + events: + items: + $ref: '#/components/schemas/Event' + type: array + has_more: + description: True if there are more events + type: boolean + through: + description: The UID of the last event returned + type: string + required: + - events + - has_more + type: object + GetEventsByCursor_200_response: + example: + next_cursor: next_cursor + has_more: true + events: + - moved: 7.457744773683766 + rsrp: 5.025004791520295 + where_when: 7.061401241503109 + body: "{}" + when: 6.027456183070403 + where_timezone: where_timezone + tri_country: tri_country + tri_points: 6.84685269835264 + where_lon: 3.616076749251911 + best_lat: 5.637376656633329 + best_timezone: best_timezone + payload: payload + where_country: where_country + tri_timezone: tri_timezone + tower_timezone: tower_timezone + sku: sku + tower_lat: 4.145608029883936 + req: req + app: app + tower_id: tower_id + where_lat: 9.301444243932576 + rat: rat + best_lon: 2.3021358869347655 + received: 0.8008281904610115 + where_olc: where_olc + tower_location: tower_location + tower_country: tower_country + sinr: 4.965218492984954 + device: device + note: note + tri_lat: 1.0246457001441578 + session: session + bssid: bssid + best_location: best_location + updates: 1.4658129805029452 + ssid: ssid + best_id: best_id + ordering_code: ordering_code + file: file + best_country: best_country + sn: sn + event: event + tower_when: 2.027123023002322 + best_location_when: 5.962133916683182 + product: product + where_location: where_location + tower_lon: 7.386281948385884 + orientation: orientation + rssi: 1.1730742509559433 + temp: 8.762042012749001 + tri_when: 1.2315135367772556 + tri_location: tri_location + transport: transport + best_location_type: best_location_type + bars: 9.369310271410669 + voltage: 6.683562403749608 + environment: "{}" + rsrq: 9.965781217890562 + tri_lon: 1.4894159098541704 + tls: true + - moved: 7.457744773683766 + rsrp: 5.025004791520295 + where_when: 7.061401241503109 + body: "{}" + when: 6.027456183070403 + where_timezone: where_timezone + tri_country: tri_country + tri_points: 6.84685269835264 + where_lon: 3.616076749251911 + best_lat: 5.637376656633329 + best_timezone: best_timezone + payload: payload + where_country: where_country + tri_timezone: tri_timezone + tower_timezone: tower_timezone + sku: sku + tower_lat: 4.145608029883936 + req: req + app: app + tower_id: tower_id + where_lat: 9.301444243932576 + rat: rat + best_lon: 2.3021358869347655 + received: 0.8008281904610115 + where_olc: where_olc + tower_location: tower_location + tower_country: tower_country + sinr: 4.965218492984954 + device: device + note: note + tri_lat: 1.0246457001441578 + session: session + bssid: bssid + best_location: best_location + updates: 1.4658129805029452 + ssid: ssid + best_id: best_id + ordering_code: ordering_code + file: file + best_country: best_country + sn: sn + event: event + tower_when: 2.027123023002322 + best_location_when: 5.962133916683182 + product: product + where_location: where_location + tower_lon: 7.386281948385884 + orientation: orientation + rssi: 1.1730742509559433 + temp: 8.762042012749001 + tri_when: 1.2315135367772556 + tri_location: tri_location + transport: transport + best_location_type: best_location_type + bars: 9.369310271410669 + voltage: 6.683562403749608 + environment: "{}" + rsrq: 9.965781217890562 + tri_lon: 1.4894159098541704 + tls: true + properties: + events: + items: + $ref: '#/components/schemas/Event' + type: array + next_cursor: + description: | + The cursor value of the next result, which is intended to be used as the "cursor" parameter value of the next call to this method. An empty string is returned if there are no more results after this results set. + type: string + has_more: + description: True if there are more events + type: boolean + required: + - events + - has_more + - next_cursor + type: object + GetRouteLogsUsage_200_response: + example: + route_logs: + - period: 2025-07-23T00:00:00Z + failed_routes: 4 + route: route:cbd20093cba58392c9f9bbdd0cdeb1a0 + successful_routes: 38 + total_routes: 42 + - period: 2025-07-23T00:00:00Z + failed_routes: 4 + route: route:cbd20093cba58392c9f9bbdd0cdeb1a0 + successful_routes: 38 + total_routes: 42 + properties: + route_logs: + items: + $ref: '#/components/schemas/UsageRouteLogsData' + type: array + required: + - route_logs + type: object + GetSessionsUsage_200_response: + example: + sessions: + - fleet: fleet:1042ddc5-3b2c-4cec-b1fb-d3040538094d + period: 2025-07-23T00:00:00Z + sessions: 12 + total_bytes: 1048576 + device: dev:123456789012345 + - fleet: fleet:1042ddc5-3b2c-4cec-b1fb-d3040538094d + period: 2025-07-23T00:00:00Z + sessions: 12 + total_bytes: 1048576 + device: dev:123456789012345 + properties: + sessions: + items: + $ref: '#/components/schemas/UsageSessionsData' + type: array + required: + - sessions + type: object + GetDataUsage_200_response_data_inner: + example: + fleet: fleet:1042ddc5-3b2c-4cec-b1fb-d3040538094d + iccid: "12345678901234567890" + data: + - period: 2025-07-23T00:00:00Z + total_bytes: 1048576 + bytes_received: 524288 + bytes_sent: 524288 + - period: 2025-07-23T00:00:00Z + total_bytes: 1048576 + bytes_received: 524288 + bytes_sent: 524288 + imsi: "123456789012345" + type: cellular + device: dev:123456789012345 + properties: + device: + description: The device UID this usage data belongs to (only present when + aggregate is 'device') + example: dev:123456789012345 + type: string + fleet: + description: The fleet UID this usage data belongs to (only present when + aggregate is 'fleet') + example: fleet:1042ddc5-3b2c-4cec-b1fb-d3040538094d + type: string + type: + description: The type of connectivity + enum: + - cellular + - satellite + type: string + iccid: + description: The ICCID of the cellular SIM card (only present when type + is 'cellular') + example: "12345678901234567890" + type: string + imsi: + description: The IMSI of the satellite device (only present when type is + 'satellite') + example: "123456789012345" + type: string + data: + items: + $ref: '#/components/schemas/UsageData' + type: array + required: + - data + - type + type: object + GetDataUsage_200_response: + example: + data: + - fleet: fleet:1042ddc5-3b2c-4cec-b1fb-d3040538094d + iccid: "12345678901234567890" + data: + - period: 2025-07-23T00:00:00Z + total_bytes: 1048576 + bytes_received: 524288 + bytes_sent: 524288 + - period: 2025-07-23T00:00:00Z + total_bytes: 1048576 + bytes_received: 524288 + bytes_sent: 524288 + imsi: "123456789012345" + type: cellular + device: dev:123456789012345 + - fleet: fleet:1042ddc5-3b2c-4cec-b1fb-d3040538094d + iccid: "12345678901234567890" + data: + - period: 2025-07-23T00:00:00Z + total_bytes: 1048576 + bytes_received: 524288 + bytes_sent: 524288 + - period: 2025-07-23T00:00:00Z + total_bytes: 1048576 + bytes_received: 524288 + bytes_sent: 524288 + imsi: "123456789012345" + type: cellular + device: dev:123456789012345 + properties: + data: + items: + $ref: '#/components/schemas/GetDataUsage_200_response_data_inner' + type: array + type: object + GetDeviceEnvironmentVariables_200_response: + example: + environment_variables: + key: environment_variables + environment_variables_effective: + key: environment_variables_effective + environment_variables_env_default: + key: environment_variables_env_default + properties: + environment_variables: + additionalProperties: + type: string + description: The environment variables for this device that have been set + using host firmware or the Notehub API or UI. + type: object + environment_variables_env_default: + additionalProperties: + type: string + description: The environment variables that have been set using the env.default + request through the Notecard API. + type: object + environment_variables_effective: + additionalProperties: + type: string + description: "The environment variables as they will be seen by the device,\ + \ fully resolved with project/fleet/device prioritization rules." + type: object + required: + - environment_variables + - environment_variables_env_default + type: object + GetDbNote_200_response: + example: + payload: payload + time: 0 + body: "{}" + properties: + body: + description: The note body + type: object + payload: + description: The note payload + type: string + time: + description: The time the Note was added to the Notecard or Notehub + type: integer + type: object + GetNotefile_200_response: + example: + total: 0 + notes: "{}" + changes: 6 + properties: + total: + description: The total number of notes. + type: integer + changes: + description: The number of pending changes in the Notefile. + type: integer + notes: + description: An object with a key for each note and a value object with + the body of each Note and the time the Note was added. + type: object + type: object + ListNotefiles_200_response: + example: + total: 0 + changes: 6 + info: "{}" + properties: + total: + description: The total number of files. + type: integer + changes: + description: The number of pending changes in the Notefile. + type: integer + info: + description: "An object with a key for each Notefile that matched the request\ + \ parameters, and value object with the changes and total for each file." + type: object + type: object + ListPendingNotefiles_200_response: + example: + total: 0 + pending: true + changes: 6 + info: "{}" + properties: + total: + description: The total number of files. + type: integer + changes: + description: The number of pending changes in the Notefile. + type: integer + pending: + description: Whether there are pending changes. + type: boolean + info: + description: "An object with a key for each Notefile that matched the request\ + \ parameters, and value object with the changes and total for each file." + type: object + type: object + DeleteNotefiles_request: + properties: + files: + description: Name of notefiles to delete + items: + type: string + type: array + type: object + SignalDevice_200_response: + example: + connected: true + properties: + connected: + description: true if the Notecard is connected to Notehub. + type: boolean + type: object + GetAlerts_200_response: + example: + alerts: + - monitor_uid: monitor_uid + data: + - alert_source: app + source_uid: source_uid + source_type: event + source: source + value: 5.962133916683182 + when: when + - alert_source: app + source_uid: source_uid + source_type: event + source: source + value: 5.962133916683182 + when: when + created_at: 0 + source: source + version: 1 + monitor_name: monitor_name + field_name: field_name + uid: uid + device_uid: device_uid + alert_source: app + value: 6.027456183070403 + monitor_type: event + notifications: + - notification_type: email + recipients: recipients + status: 5.637376656633329 + - notification_type: email + recipients: recipients + status: 5.637376656633329 + resolved: true + - monitor_uid: monitor_uid + data: + - alert_source: app + source_uid: source_uid + source_type: event + source: source + value: 5.962133916683182 + when: when + - alert_source: app + source_uid: source_uid + source_type: event + source: source + value: 5.962133916683182 + when: when + created_at: 0 + source: source + version: 1 + monitor_name: monitor_name + field_name: field_name + uid: uid + device_uid: device_uid + alert_source: app + value: 6.027456183070403 + monitor_type: event + notifications: + - notification_type: email + recipients: recipients + status: 5.637376656633329 + - notification_type: email + recipients: recipients + status: 5.637376656633329 + resolved: true + has_more: true + properties: + alerts: + description: The list of alerts + items: + $ref: '#/components/schemas/Alert' + type: array + has_more: + description: True if there are more alerts + type: boolean + required: + - alerts + - has_more + type: object + GetWebhooks_200_response: + example: + webhooks: + - transform: transform + disabled: true + id: id + - transform: transform + disabled: true + id: id + properties: + webhooks: + items: + $ref: '#/components/schemas/WebhookSettings' + type: array + type: object + PersonalAccessToken_created_by: + description: The user that created this key + example: + uid: uid + name: name + email: email + properties: + uid: + type: string + email: + type: string + name: + type: string + type: object + Device_tower_info: + example: + mnc: 6 + mcc: 0 + cell_id: 5 + lac: 1 + nullable: true + properties: + mcc: + type: integer + mnc: + type: integer + lac: + type: integer + cell_id: + type: integer + type: object + Monitor_alert_routes_inner: + oneOf: + - $ref: '#/components/schemas/SlackWebHookNotification' + - $ref: '#/components/schemas/SlackBearerNotification' + - $ref: '#/components/schemas/EmailNotification' + Alert_data_inner: + example: + alert_source: app + source_uid: source_uid + source_type: event + source: source + value: 5.962133916683182 + when: when + properties: + alert_source: + description: The source of the alert + enum: + - app + - device + type: string + source: + description: The UID of the source of the alert + type: string + source_type: + description: The type of source. + enum: + - event + type: string + value: + description: The value that triggered the alert + type: number + source_uid: + description: The UID of the source of the alert + type: string + when: + description: The time the alert was created + type: string + type: object + Alert_notifications_inner: + example: + notification_type: email + recipients: recipients + status: 5.637376656633329 + properties: + notification_type: + description: The type of notification + enum: + - email + - slack + type: string + status: + description: The status of the notification + type: number + recipients: + description: The recipients of the notification + type: string + type: object + DeviceDfuStatus_current: + description: Description of the current firmware + example: + product: product + built: built + organization: organization + builder: builder + description: description + version: version + properties: + version: + description: Firmware version + type: string + organization: + description: Firmware organization + type: string + description: + description: Firmware description + type: string + product: + description: Firmware product + type: string + built: + description: Firmware build date + type: string + builder: + description: Firmware author + type: string + type: object + securitySchemes: + personalAccessToken: + description: | + Use a personal access token from notehub.io/api-access + scheme: bearer + type: http + pin: + description: | + For accessing endpoints by Device pin. + in: header + name: X-Auth-Token + type: apiKey diff --git a/notehub/cmd/product.go b/notehub/cmd/product.go new file mode 100644 index 0000000..54be981 --- /dev/null +++ b/notehub/cmd/product.go @@ -0,0 +1,158 @@ +// Copyright 2025 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package cmd + +import ( + "fmt" + + "github.com/blues/note-go/note" + notehub "github.com/blues/notehub-go" + "github.com/spf13/cobra" +) + +// productCmd represents the product command +var productCmd = &cobra.Command{ + Use: "product", + Short: "Manage Notehub products", + Long: `Commands for listing and managing products in Notehub projects.`, +} + +// productListCmd represents the product list command +var productListCmd = &cobra.Command{ + Use: "list", + Short: "List all products in a project", + Long: `List all products in the current project or a specified project.`, + RunE: func(cmd *cobra.Command, args []string) error { + GetCredentials() // Validates and exits if not authenticated + + // Get project UID (from config or --project flag) + projectUID := GetProject() + if projectUID == "" { + return fmt.Errorf("no project set. Use 'notehub project set ' or provide --project flag") + } + + // Get products using SDK + client := GetNotehubClient() + ctx, err := GetNotehubContext() + if err != nil { + return err + } + + productsRsp, _, err := client.ProjectAPI.GetProducts(ctx, projectUID).Execute() + if err != nil { + return fmt.Errorf("failed to list products: %w", err) + } + + // Handle JSON output + if GetJson() || GetPretty() { + var output []byte + var err error + if GetPretty() { + output, err = note.JSONMarshalIndent(productsRsp, "", " ") + } else { + output, err = note.JSONMarshal(productsRsp) + } + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Printf("%s\n", output) + return nil + } + + if len(productsRsp.Products) == 0 { + fmt.Println("No products found in this project.") + return nil + } + + // Display products in human-readable format + fmt.Printf("\nProducts in Project:\n") + fmt.Printf("====================\n\n") + + for _, product := range productsRsp.Products { + fmt.Printf(" %s\n", product.Label) + fmt.Printf(" %s\n\n", product.Uid) + } + + fmt.Printf("Total products: %d\n\n", len(productsRsp.Products)) + + return nil + }, +} + +// productGetCmd represents the product get command +var productGetCmd = &cobra.Command{ + Use: "get [product-uid-or-name]", + Short: "Get details about a specific product", + Long: `Get detailed information about a specific product by UID or name.`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + GetCredentials() // Validates and exits if not authenticated + + productIdentifier := args[0] + + // Get project UID (from config or --project flag) + projectUID := GetProject() + if projectUID == "" { + return fmt.Errorf("no project set. Use 'notehub project set ' or provide --project flag") + } + + // Get SDK client + client := GetNotehubClient() + ctx, err := GetNotehubContext() + if err != nil { + return err + } + + // Get all products and find the matching one + productsRsp, _, err := client.ProjectAPI.GetProducts(ctx, projectUID).Execute() + if err != nil { + return fmt.Errorf("failed to list products: %w", err) + } + + // Find the product by UID or name + var foundProduct *notehub.Product + for _, product := range productsRsp.Products { + if product.Uid == productIdentifier || product.Label == productIdentifier { + foundProduct = &product + break + } + } + + if foundProduct == nil { + return fmt.Errorf("product '%s' not found in project", productIdentifier) + } + + // Handle JSON output + if GetJson() || GetPretty() { + var output []byte + var err error + if GetPretty() { + output, err = note.JSONMarshalIndent(foundProduct, "", " ") + } else { + output, err = note.JSONMarshal(foundProduct) + } + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Printf("%s\n", output) + return nil + } + + // Display product in human-readable format + fmt.Printf("\nProduct Details:\n") + fmt.Printf("================\n\n") + fmt.Printf("Name: %s\n", foundProduct.Label) + fmt.Printf("UID: %s\n", foundProduct.Uid) + fmt.Println() + + return nil + }, +} + +func init() { + rootCmd.AddCommand(productCmd) + productCmd.AddCommand(productListCmd) + productCmd.AddCommand(productGetCmd) +} diff --git a/notehub/cmd/project.go b/notehub/cmd/project.go new file mode 100644 index 0000000..047f436 --- /dev/null +++ b/notehub/cmd/project.go @@ -0,0 +1,302 @@ +// Copyright 2025 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package cmd + +import ( + "fmt" + + "github.com/blues/note-go/note" + notehub "github.com/blues/notehub-go" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// projectCmd represents the project command +var projectCmd = &cobra.Command{ + Use: "project", + Short: "Manage Notehub projects", + Long: `Commands for listing and selecting Notehub projects to work with.`, +} + +// projectListCmd represents the project list command +var projectListCmd = &cobra.Command{ + Use: "list", + Short: "List all projects", + Long: `List all Notehub projects for the authenticated user.`, + RunE: func(cmd *cobra.Command, args []string) error { + credentials := GetCredentials() // Validates and exits if not authenticated + + // Get all projects using SDK + client := GetNotehubClient() + ctx, err := GetNotehubContext() + if err != nil { + return err + } + + projectsRsp, _, err := client.ProjectAPI.GetProjects(ctx).Execute() + if err != nil { + return fmt.Errorf("failed to list projects: %w", err) + } + + // Handle JSON output + if GetJson() || GetPretty() { + var output []byte + var err error + if GetPretty() { + output, err = note.JSONMarshalIndent(projectsRsp, "", " ") + } else { + output, err = note.JSONMarshal(projectsRsp) + } + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Printf("%s\n", output) + return nil + } + + if len(projectsRsp.Projects) == 0 { + fmt.Println("No projects found.") + fmt.Println("\nYou can create a new project at https://notehub.io") + return nil + } + + // Check current project + currentProject := GetProject() + + // Display projects in human-readable format + fmt.Println("\nAvailable Projects:") + fmt.Println("===================") + for _, project := range projectsRsp.Projects { + if project.Uid == currentProject { + fmt.Printf("* %s (current)\n", project.Label) + fmt.Printf(" %s\n\n", project.Uid) + } else { + fmt.Printf(" %s\n", project.Label) + fmt.Printf(" %s\n\n", project.Uid) + } + } + + if currentProject == "" { + fmt.Println("No project selected. Use 'notehub project set ' to select one.") + } + + // Show credentials user + fmt.Printf("Signed in as: %s\n\n", credentials.User) + + return nil + }, +} + +// projectSetCmd represents the project set command +var projectSetCmd = &cobra.Command{ + Use: "set [project-name-or-uid]", + Short: "Set the active project", + Long: `Set the active project in the configuration. You can specify either the project name or UID.`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + GetCredentials() // Validates and exits if not authenticated + + projectIdentifier := args[0] + + // Get SDK client + client := GetNotehubClient() + ctx, err := GetNotehubContext() + if err != nil { + return err + } + + // First, try to use it directly as a UID + var selectedProject notehub.Project + project, resp, err := client.ProjectAPI.GetProject(ctx, projectIdentifier).Execute() + + // If that failed, it might be a project name - fetch all projects and search + if err != nil || (resp != nil && resp.StatusCode == 404) { + projectsRsp, _, err := client.ProjectAPI.GetProjects(ctx).Execute() + if err != nil { + return fmt.Errorf("failed to list projects: %w", err) + } + + // Search for project by name (exact match) + found := false + for _, proj := range projectsRsp.Projects { + if proj.Label == projectIdentifier { + selectedProject = proj + found = true + break + } + } + + if !found { + return fmt.Errorf("project '%s' not found. Use 'notehub project list' to see available projects", projectIdentifier) + } + } else { + selectedProject = *project + } + + // Save to config + viper.Set("project", selectedProject.Uid) + if err := SaveConfig(); err != nil { + return fmt.Errorf("failed to save config: %w", err) + } + + fmt.Printf("Active project set to: %s\n", selectedProject.Label) + fmt.Printf("Project UID: %s\n", selectedProject.Uid) + fmt.Println("\nThis project will now be used as the default for all commands.") + + return nil + }, +} + +// projectGetCmd represents the project get command +var projectGetCmd = &cobra.Command{ + Use: "get [project-name-or-uid]", + Short: "Get detailed information about a project", + Long: `Get detailed information about a specific project. If no project is specified, uses the active project. + +Examples: + # Get information about active project + notehub project get + + # Get information about specific project by UID + notehub project get app:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + + # Get information about specific project by name + notehub project get "My Project" + + # Get with JSON output + notehub project get --json + + # Get with pretty JSON + notehub project get --pretty`, + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + GetCredentials() // Validates and exits if not authenticated + + var projectUID string + + // Get SDK client + client := GetNotehubClient() + ctx, err := GetNotehubContext() + if err != nil { + return err + } + + // If project specified, use that; otherwise use active project + if len(args) > 0 { + projectIdentifier := args[0] + + // First try to use it directly as a UID + if len(projectIdentifier) > 4 && projectIdentifier[:4] == "app:" { + projectUID = projectIdentifier + } else { + // It might be a project name - fetch all projects and search + projectsRsp, _, err := client.ProjectAPI.GetProjects(ctx).Execute() + if err != nil { + return fmt.Errorf("failed to list projects: %w", err) + } + + // Search for project by name (exact match) + found := false + for _, project := range projectsRsp.Projects { + if project.Label == projectIdentifier { + projectUID = project.Uid + found = true + break + } + } + + if !found { + return fmt.Errorf("project '%s' not found. Use 'notehub project list' to see available projects", projectIdentifier) + } + } + } else { + // Use active project + projectUID = GetProject() + if projectUID == "" { + return fmt.Errorf("no project specified and no active project set. Use 'notehub project set ' to set an active project") + } + } + + // Get project details using SDK + project, _, err := client.ProjectAPI.GetProject(ctx, projectUID).Execute() + if err != nil { + return fmt.Errorf("failed to get project: %w", err) + } + + // Handle JSON output + if GetJson() || GetPretty() { + var output []byte + var err error + if GetPretty() { + output, err = note.JSONMarshalIndent(project, "", " ") + } else { + output, err = note.JSONMarshal(project) + } + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Printf("%s\n", output) + return nil + } + + // Display project in human-readable format + // Check if this is the active project + currentProject := GetProject() + isActive := (project.Uid == currentProject) + + fmt.Printf("\nProject Details:\n") + fmt.Printf("================\n\n") + fmt.Printf("Name: %s", project.Label) + if isActive { + fmt.Printf(" (active)") + } + fmt.Println() + fmt.Printf("UID: %s\n", project.Uid) + if !project.Created.IsZero() { + fmt.Printf("Created: %s\n", project.Created.Format("2006-01-02 15:04:05 MST")) + } + if project.Role.IsSet() { + if role := project.Role.Get(); role != nil { + fmt.Printf("Role: %s\n", string(*role)) + } + } + fmt.Println() + + return nil + }, +} + +// projectClearCmd represents the project clear command +var projectClearCmd = &cobra.Command{ + Use: "clear", + Short: "Clear the active project", + Long: `Clear the active project from the configuration.`, + RunE: func(cmd *cobra.Command, args []string) error { + currentProject := GetProject() + if currentProject == "" { + fmt.Println("No project is currently set.") + return nil + } + + // Clear from config + viper.Set("project", "") + if err := SaveConfig(); err != nil { + return fmt.Errorf("failed to save config: %w", err) + } + + fmt.Println("Active project cleared.") + fmt.Println("You can set a new project with 'notehub project set '") + + return nil + }, +} + +func init() { + rootCmd.AddCommand(projectCmd) + projectCmd.AddCommand(projectListCmd) + projectCmd.AddCommand(projectGetCmd) + projectCmd.AddCommand(projectSetCmd) + projectCmd.AddCommand(projectClearCmd) +} diff --git a/notehub/cmd/provision.go b/notehub/cmd/provision.go new file mode 100644 index 0000000..0ea7118 --- /dev/null +++ b/notehub/cmd/provision.go @@ -0,0 +1,85 @@ +// Copyright 2025 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// provisionCmd represents the provision command +var provisionCmd = &cobra.Command{ + Use: "provision", + Short: "Provision devices to a product", + Long: `Provision devices to a product. + +The --product flag specifies the target product UID. The --project flag (or +config file) specifies which project contains the devices to provision. +Command-line flags override config file values. + +Scope Formats: + dev:xxxx Single device UID + imei:xxxx Device by IMEI + fleet:xxxx All devices in fleet (by UID) + production All devices in named fleet + @fleet-name All devices in fleet (indirection) + @ All devices in project + @devices.txt Device UIDs from file (one per line) + dev:aaa,dev:bbb Multiple scopes (comma-separated) + +Examples: + # Provision a single device (project from config) + notehub provision --scope dev:864475046552567 --product com.company:product + + # Provision all devices in a fleet + notehub provision --scope @production --product com.company:product + + # Provision all devices in project + notehub provision --scope @ --product com.company:product + + # Provision devices from a file + notehub provision --scope @devices.txt --product com.company:product + + # Override project from command line + notehub provision --scope dev:xxxx --product com.company:product --project app:xxxx + + # Provision with serial number + notehub provision --scope dev:xxxx --product com.company:product --sn SENSOR-001`, + RunE: func(cmd *cobra.Command, args []string) error { + GetCredentials() // Validate credentials + + if flagScope == "" { + return fmt.Errorf("use --scope to specify device(s) to be provisioned") + } + + product := GetProduct() + if product == "" { + return fmt.Errorf("--product must be specified (the product UID to provision devices to)") + } + + appMetadata, scopeDevices, _, err := ResolveScopeWithValidation(flagScope) + if err != nil { + return err + } + + verbose := GetVerbose() + err = varsProvisionDevices(appMetadata, scopeDevices, product, flagSn, verbose) + if err != nil { + return err + } + + fmt.Printf("Successfully provisioned %d device(s) to product %s\n", len(scopeDevices), product) + return nil + }, +} + +func init() { + rootCmd.AddCommand(provisionCmd) + + provisionCmd.Flags().StringVarP(&flagScope, "scope", "s", "", "Device scope (required)") + provisionCmd.Flags().StringVar(&flagSn, "sn", "", "Serial number for provisioning") + provisionCmd.MarkFlagRequired("scope") +} diff --git a/notehub/req.go b/notehub/cmd/req.go similarity index 75% rename from notehub/req.go rename to notehub/cmd/req.go index d60b5b7..5b007d9 100644 --- a/notehub/req.go +++ b/notehub/cmd/req.go @@ -1,8 +1,8 @@ -// Copyright 2017 Blues Inc. All rights reserved. +// Copyright 2025 Blues Inc. All rights reserved. // Use of this source code is governed by licenses granted by the // copyright holder including that found in the LICENSE file. -package main +package cmd import ( "bytes" @@ -13,11 +13,15 @@ import ( "os" "strings" - "github.com/blues/note-cli/lib" "github.com/blues/note-go/note" "github.com/blues/note-go/notehub" ) +// Used by req functions +var reqFlagApp string +var reqFlagProduct string +var reqFlagDevice string + // Add an arg to an URL query string func addQuery(in string, key string, value string) (out string) { out = in @@ -35,14 +39,16 @@ func addQuery(in string, key string, value string) (out string) { return } -// Perform a hub transaction, and promote the returned err response to an error to this method +// Perform a hub transaction using V0 Notecard API format, and promote the returned err response to an error to this method +// Note: This is used for device-specific Notecard communication APIs (file.changes, note.changes, etc.) +// which are distinct from Notehub project management APIs that use V1 REST endpoints. func hubTransactionRequest(request notehub.HubRequest, verbose bool) (rsp notehub.HubRequest, err error) { var reqJSON []byte reqJSON, err = note.JSONMarshal(request) if err != nil { return } - err = reqHubV0(verbose, lib.ConfigAPIHub(), reqJSON, "", "", "", "", false, false, nil, &rsp) + err = reqHubV0(verbose, GetAPIHub(), reqJSON, "", "", "", "", false, false, nil, &rsp) if err != nil { return } @@ -53,6 +59,8 @@ func hubTransactionRequest(request notehub.HubRequest, verbose bool) (rsp notehu } // Process a V0 HTTPS request and unmarshal into an object +// Note: V0 API is used for device-specific Notecard communication (file.changes, note.changes, etc.) +// For Notehub project management operations, use reqHubV1 instead. func reqHubV0(verbose bool, hub string, request []byte, requestFile string, filetype string, filetags string, filenotes string, overwrite bool, dropNonJSON bool, outq chan string, object interface{}) (err error) { var response []byte response, err = reqHubV0JSON(verbose, hub, request, requestFile, filetype, filetags, filenotes, overwrite, dropNonJSON, outq) @@ -67,7 +75,6 @@ func reqHubV0(verbose bool, hub string, request []byte, requestFile string, file // Perform a V0 HTTP request func reqHubV0JSON(verbose bool, hub string, request []byte, requestFile string, filetype string, filetags string, filenotes string, overwrite bool, dropNonJSON bool, outq chan string) (response []byte, err error) { - fn := "" path := strings.Split(requestFile, "/") if len(path) > 0 { @@ -75,15 +82,15 @@ func reqHubV0JSON(verbose bool, hub string, request []byte, requestFile string, } if hub == "" { - hub = lib.ConfigAPIHub() + hub = GetAPIHub() } httpurl := fmt.Sprintf("https://%s/req", hub) - query := addQuery("", "app", flagApp) - if flagApp == "" { - query = addQuery("", "product", flagProduct) + query := addQuery("", "app", reqFlagApp) + if reqFlagApp == "" { + query = addQuery("", "product", reqFlagProduct) } - query = addQuery(query, "device", flagDevice) + query = addQuery(query, "device", reqFlagDevice) query = addQuery(query, "upload", fn) if overwrite { query = addQuery(query, "overwrite", "true") @@ -123,7 +130,7 @@ func reqHubV0JSON(verbose bool, hub string, request []byte, requestFile string, httpReq.Header.Set("Content-Type", "application/json") } - err = lib.ConfigAuthenticationHeader(httpReq) + err = AddAuthenticationHeader(httpReq) if err != nil { return } @@ -146,18 +153,13 @@ func reqHubV0JSON(verbose bool, hub string, request []byte, requestFile string, for { n, err2 := httpRsp.Body.Read(b) if n > 0 { - // Append to result buffer if no outq is specified if outq == nil { - response = append(response, b[:n]...) - } else { - // Enqueue lines for monitoring linebuf = append(linebuf, b[:n]...) for { - // Parse out a full line and queue it, saving the leftover i := bytes.IndexRune(linebuf, '\n') if i == -1 { @@ -177,9 +179,7 @@ func reqHubV0JSON(verbose bool, hub string, request []byte, requestFile string, // was an error and we're about to get an io.EOF response = line } - } - } if err2 != nil { if err2 != io.EOF { @@ -195,10 +195,11 @@ func reqHubV0JSON(verbose bool, hub string, request []byte, requestFile string, } return - } // Process a V1 HTTPS request and unmarshal into an object +// Note: V1 REST API is used for Notehub project management operations (projects, devices, fleets, routes, etc.) +// For device-specific Notecard communication, use reqHubV0 instead. func reqHubV1(verbose bool, hub string, verb string, url string, body []byte, object interface{}) (err error) { var response []byte response, err = reqHubV1JSON(verbose, hub, verb, url, body) @@ -213,7 +214,6 @@ func reqHubV1(verbose bool, hub string, verb string, url string, body []byte, ob // Process an HTTPS request func reqHubV1JSON(verbose bool, hub string, verb string, url string, body []byte) (response []byte, err error) { - verb = strings.ToUpper(verb) httpurl := fmt.Sprintf("https://%s%s", hub, url) @@ -227,7 +227,7 @@ func reqHubV1JSON(verbose bool, hub string, verb string, url string, body []byte } httpReq.Header.Set("User-Agent", "notehub-client") httpReq.Header.Set("Content-Type", "application/json") - err = lib.ConfigAuthenticationHeader(httpReq) + err = AddAuthenticationHeader(httpReq) if err != nil { return } @@ -245,11 +245,6 @@ func reqHubV1JSON(verbose bool, hub string, verb string, url string, body []byte err = err2 return } - if httpRsp.StatusCode == http.StatusUnauthorized { - err = fmt.Errorf("please use -signin to authenticate") - return - } - if verbose { fmt.Printf("STATUS %d\n", httpRsp.StatusCode) } @@ -263,6 +258,28 @@ func reqHubV1JSON(verbose bool, hub string, verb string, url string, body []byte fmt.Printf("%s\n", string(response)) } - return + // Check for HTTP error status codes + if httpRsp.StatusCode == http.StatusUnauthorized { + err = fmt.Errorf("please use -signin to authenticate") + return + } + // Check for other HTTP error status codes (4xx, 5xx) + if httpRsp.StatusCode >= 400 { + // Try to parse error message from response body + if len(response) > 0 { + var errResp map[string]interface{} + if unmarshalErr := note.JSONUnmarshal(response, &errResp); unmarshalErr == nil { + if errMsg, ok := errResp["err"].(string); ok { + err = fmt.Errorf("HTTP %d: %s", httpRsp.StatusCode, errMsg) + return + } + } + } + // Fallback to generic error if we couldn't parse the error message + err = fmt.Errorf("HTTP %d: %s", httpRsp.StatusCode, http.StatusText(httpRsp.StatusCode)) + return + } + + return } diff --git a/notehub/cmd/request.go b/notehub/cmd/request.go new file mode 100644 index 0000000..51e8a31 --- /dev/null +++ b/notehub/cmd/request.go @@ -0,0 +1,90 @@ +// Copyright 2025 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package cmd + +import ( + "fmt" + "os" + "strings" + + "github.com/blues/note-go/note" + "github.com/spf13/cobra" +) + +var ( + flagOut string +) + +// requestCmd represents the request command +var requestCmd = &cobra.Command{ + Use: "request [json]", + Aliases: []string{"req"}, + Short: "Send an API request to Notehub", + Long: `Send a raw API request to Notehub. + +The JSON argument can be a JSON object or @filename to read from a file. + +Example: + notehub request '{"req":"hub.app.get"}' --project app:xxxx + notehub req @request.json --device dev:xxxx --pretty`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + GetCredentials() // Validate credentials + + reqArg := args[0] + + // Process request starting with @ as a filename + if strings.HasPrefix(reqArg, "@") { + fn := strings.TrimPrefix(reqArg, "@") + contents, err := os.ReadFile(fn) + if err != nil { + return fmt.Errorf("can't read request file '%s': %w", fn, err) + } + reqArg = string(contents) + } + + // Set flags for the request functions + reqFlagApp = GetProject() + reqFlagProduct = GetProduct() + reqFlagDevice = GetDevice() + + // Perform the request + rsp, err := reqHubV0JSON(GetVerbose(), GetAPIHub(), []byte(reqArg), "", "", "", "", false, GetJson(), nil) + if err != nil { + return err + } + + // Handle output + if flagOut == "" { + if GetPretty() { + var rspo map[string]interface{} + err = note.JSONUnmarshal(rsp, &rspo) + if err != nil { + fmt.Printf("%s", rsp) + } else { + rsp, _ = note.JSONMarshalIndent(rspo, "", " ") + fmt.Printf("%s", rsp) + } + } else { + fmt.Printf("%s", rsp) + } + } else { + outfile, err := os.Create(flagOut) + if err != nil { + return err + } + defer outfile.Close() + outfile.Write(rsp) + } + + return nil + }, +} + +func init() { + rootCmd.AddCommand(requestCmd) + + requestCmd.Flags().StringVarP(&flagOut, "out", "o", "", "Output filename") +} diff --git a/notehub/cmd/root.go b/notehub/cmd/root.go new file mode 100644 index 0000000..00f4c81 --- /dev/null +++ b/notehub/cmd/root.go @@ -0,0 +1,164 @@ +// Copyright 2025 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package cmd + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// CLI Version - Set by ldflags during build/release +var version = "development" + +// Global flags +var ( + flagProject string + flagProduct string + flagDevice string + flagVerbose bool + flagPretty bool + flagJson bool +) + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "notehub", + Short: "Notehub CLI - Command-line tool for interacting with Notehub", + Long: `Notehub CLI is a command-line tool for interacting with Blues Notehub. + +It provides commands for authentication, managing projects and devices, +setting environment variables, and making API requests.`, + Version: version, + Run: func(cmd *cobra.Command, args []string) { + // If no subcommand is provided, print help (config shown via HelpFunc) + cmd.Help() + }, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + if err := rootCmd.Execute(); err != nil { + os.Exit(1) + } +} + +func init() { + // Initialize config before running any command + cobra.OnInitialize(initConfig) + + // Global flags available to all commands + rootCmd.PersistentFlags().StringVarP(&flagProject, "project", "p", "", "Project UID") + rootCmd.PersistentFlags().StringVar(&flagProduct, "product", "", "Product UID") + rootCmd.PersistentFlags().StringVarP(&flagDevice, "device", "d", "", "Device UID") + rootCmd.PersistentFlags().BoolVarP(&flagVerbose, "verbose", "v", false, "Display requests and responses") + rootCmd.PersistentFlags().BoolVar(&flagPretty, "pretty", false, "Pretty print JSON output") + rootCmd.PersistentFlags().BoolVar(&flagJson, "json", false, "Strip all non-JSON lines from output") + + // Bind flags to Viper (allows flags to override config file values) + viper.BindPFlag("project", rootCmd.PersistentFlags().Lookup("project")) + viper.BindPFlag("product", rootCmd.PersistentFlags().Lookup("product")) + viper.BindPFlag("device", rootCmd.PersistentFlags().Lookup("device")) + viper.BindPFlag("verbose", rootCmd.PersistentFlags().Lookup("verbose")) + viper.BindPFlag("pretty", rootCmd.PersistentFlags().Lookup("pretty")) + viper.BindPFlag("json", rootCmd.PersistentFlags().Lookup("json")) + + // Enable environment variable support (NOTEHUB_PROJECT, NOTEHUB_DEVICE, etc.) + viper.SetEnvPrefix("NOTEHUB") + viper.AutomaticEnv() +} + +// initConfig reads in config file and ENV variables if set. +func initConfig() { + // Get user home directory + home, err := os.UserHomeDir() + if err != nil { + fmt.Printf("Error getting home directory: %s\n", err) + os.Exit(1) + } + + // Set config file location + configDir := filepath.Join(home, ".notehub") + viper.AddConfigPath(configDir) + viper.SetConfigName("config") + viper.SetConfigType("yaml") + + // Set defaults + viper.SetDefault("hub", "notehub.io") + + // Attempt to read config file + if err := viper.ReadInConfig(); err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); ok { + // Config file not found; create with defaults + if err := os.MkdirAll(configDir, 0755); err != nil { + fmt.Printf("Error creating config directory: %s\n", err) + os.Exit(1) + } + // Write default config + if err := SaveConfig(); err != nil { + fmt.Printf("Error creating config file: %s\n", err) + os.Exit(1) + } + } else { + // Config file was found but another error was produced + fmt.Printf("Error reading config file: %s\n", err) + os.Exit(1) + } + } +} + +// GetCredentials returns validated credentials or exits with error +func GetCredentials() *Credentials { + credentials, err := GetHubCredentials() + if err != nil { + fmt.Printf("error getting credentials: %s\n", err) + os.Exit(1) + } + + if credentials == nil { + fmt.Printf("please sign in using 'notehub auth signin' or 'notehub auth signin-token'\n") + os.Exit(1) + } + + if err := credentials.Validate(); err != nil { + hub := GetHub() + fmt.Printf("invalid credentials for %s: %s\n", hub, err) + fmt.Printf("please use 'notehub auth signin' or 'notehub auth signin-token' to sign into Notehub\n") + os.Exit(1) + } + + return credentials +} + +// Helper functions to get flag values from Viper +// These allow flags, config file, and environment variables to work together + +func GetProject() string { + return viper.GetString("project") +} + +func GetProduct() string { + return viper.GetString("product") +} + +func GetDevice() string { + return viper.GetString("device") +} + +func GetVerbose() bool { + return viper.GetBool("verbose") +} + +func GetPretty() bool { + return viper.GetBool("pretty") +} + +func GetJson() bool { + return viper.GetBool("json") +} diff --git a/notehub/cmd/route.go b/notehub/cmd/route.go new file mode 100644 index 0000000..b378831 --- /dev/null +++ b/notehub/cmd/route.go @@ -0,0 +1,738 @@ +// Copyright 2025 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package cmd + +import ( + "fmt" + "os" + + "github.com/blues/note-go/note" + notehub "github.com/blues/notehub-go" + "github.com/spf13/cobra" +) + +// routeCmd represents the route command +var routeCmd = &cobra.Command{ + Use: "route", + Short: "Manage routes", + Long: `Commands for creating, updating, deleting, and viewing routes in Notehub projects.`, +} + +// routeListCmd represents the route list command +var routeListCmd = &cobra.Command{ + Use: "list", + Short: "List all routes", + Long: `List all routes in the current or specified project. + +Examples: + # List all routes + notehub route list + + # List routes in specific project + notehub route list --project app:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + + # List with JSON output + notehub route list --json + + # List with pretty JSON + notehub route list --pretty`, + RunE: func(cmd *cobra.Command, args []string) error { + GetCredentials() // Validates and exits if not authenticated + + // Get project UID (from config or --project flag) + projectUID := GetProject() + if projectUID == "" { + return fmt.Errorf("no project set. Use 'notehub project set ' or provide --project flag") + } + + // Get routes using SDK + client := GetNotehubClient() + ctx, err := GetNotehubContext() + if err != nil { + return err + } + + routes, _, err := client.RouteAPI.GetRoutes(ctx, projectUID).Execute() + if err != nil { + return fmt.Errorf("failed to list routes: %w", err) + } + + // Handle JSON output + if GetJson() || GetPretty() { + var output []byte + var err error + if GetPretty() { + output, err = note.JSONMarshalIndent(routes, "", " ") + } else { + output, err = note.JSONMarshal(routes) + } + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Printf("%s\n", output) + return nil + } + + if len(routes) == 0 { + fmt.Println("No routes found.") + return nil + } + + // Display routes in human-readable format + fmt.Printf("\nRoutes (%d):\n", len(routes)) + fmt.Printf("============\n\n") + + for _, route := range routes { + uid := "" + if route.Uid != nil { + uid = *route.Uid + } + label := "" + if route.Label != nil { + label = *route.Label + } + routeType := "" + if route.Type != nil { + routeType = *route.Type + } + modified := "" + if route.Modified != nil { + modified = route.Modified.Format("2006-01-02 15:04:05 MST") + } + disabled := false + if route.Disabled != nil { + disabled = *route.Disabled + } + + fmt.Printf("UID: %s\n", uid) + fmt.Printf(" Label: %s\n", label) + fmt.Printf(" Type: %s\n", routeType) + fmt.Printf(" Modified: %s\n", modified) + if disabled { + fmt.Printf(" Status: Disabled\n") + } else { + fmt.Printf(" Status: Enabled\n") + } + fmt.Println() + } + + return nil + }, +} + +// routeGetCmd represents the route get command +var routeGetCmd = &cobra.Command{ + Use: "get [route-uid-or-name]", + Short: "Get detailed information about a specific route", + Long: `Get detailed information about a specific route by UID or name. + +Examples: + # Get route by UID + notehub route get route:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + + # Get route by name + notehub route get "My Route" + + # Get with pretty JSON + notehub route get "My Route" --pretty`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + GetCredentials() // Validates and exits if not authenticated + + routeIdentifier := args[0] + + // Get project UID (from config or --project flag) + projectUID := GetProject() + if projectUID == "" { + return fmt.Errorf("no project set. Use 'notehub project set ' or provide --project flag") + } + + // Get SDK client + client := GetNotehubClient() + ctx, err := GetNotehubContext() + if err != nil { + return err + } + + // Determine route UID (handle both UID and name) + var routeUID string + var selectedRoute *notehub.NotehubRoute + + // First try as UID + if len(routeIdentifier) > 6 && routeIdentifier[:6] == "route:" { + route, resp, err := client.RouteAPI.GetRoute(ctx, projectUID, routeIdentifier).Execute() + if err == nil && resp != nil && resp.StatusCode != 404 { + routeUID = routeIdentifier + selectedRoute = route + } + } + + // If not found as UID, search by name + if selectedRoute == nil { + routes, _, err := client.RouteAPI.GetRoutes(ctx, projectUID).Execute() + if err != nil { + return fmt.Errorf("failed to list routes: %w", err) + } + + for _, route := range routes { + if route.Label != nil && *route.Label == routeIdentifier { + if route.Uid != nil { + routeUID = *route.Uid + // Get full route details + fullRoute, _, err := client.RouteAPI.GetRoute(ctx, projectUID, routeUID).Execute() + if err != nil { + return fmt.Errorf("failed to get route: %w", err) + } + selectedRoute = fullRoute + break + } + } + } + } + + if selectedRoute == nil { + return fmt.Errorf("route '%s' not found", routeIdentifier) + } + + // Handle JSON output + if GetJson() || GetPretty() { + var output []byte + var err error + if GetPretty() { + output, err = note.JSONMarshalIndent(selectedRoute, "", " ") + } else { + output, err = note.JSONMarshal(selectedRoute) + } + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Printf("%s\n", output) + return nil + } + + // Display route in human-readable format + uid := "" + if selectedRoute.Uid != nil { + uid = *selectedRoute.Uid + } + label := "" + if selectedRoute.Label != nil { + label = *selectedRoute.Label + } + routeType := "" + if selectedRoute.Type != nil { + routeType = *selectedRoute.Type + } + modified := "" + if selectedRoute.Modified != nil { + modified = selectedRoute.Modified.Format("2006-01-02 15:04:05 MST") + } + disabled := false + if selectedRoute.Disabled != nil { + disabled = *selectedRoute.Disabled + } + + fmt.Printf("\nRoute Details:\n") + fmt.Printf("==============\n\n") + fmt.Printf("UID: %s\n", uid) + fmt.Printf("Label: %s\n", label) + fmt.Printf("Type: %s\n", routeType) + fmt.Printf("Modified: %s\n", modified) + if disabled { + fmt.Printf("Status: Disabled\n") + } else { + fmt.Printf("Status: Enabled\n") + } + fmt.Println() + + // Display type-specific configuration + if routeType == "http" && selectedRoute.Http != nil { + fmt.Printf("HTTP Configuration:\n") + if selectedRoute.Http.Url != nil { + fmt.Printf(" URL: %s\n", *selectedRoute.Http.Url) + } + if selectedRoute.Http.Fleets != nil && len(selectedRoute.Http.Fleets) > 0 { + fmt.Printf(" Fleets: %v\n", selectedRoute.Http.Fleets) + } + if selectedRoute.Http.ThrottleMs != nil { + fmt.Printf(" Throttle: %d ms\n", *selectedRoute.Http.ThrottleMs) + } + if selectedRoute.Http.Timeout != nil && *selectedRoute.Http.Timeout > 0 { + fmt.Printf(" Timeout: %d ms\n", *selectedRoute.Http.Timeout) + } + } + + return nil + }, +} + +// routeCreateCmd represents the route create command +var routeCreateCmd = &cobra.Command{ + Use: "create [label]", + Short: "Create a new route", + Long: `Create a new route in the current project. + +Note: Route creation requires a JSON configuration file. Use --config to specify the file. + +Examples: + # Create route from JSON file + notehub route create "My Route" --config route.json + + # Example route.json for HTTP route: + { + "label": "My HTTP Route", + "http": { + "url": "https://example.com/webhook", + "fleets": ["fleet:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"], + "throttle_ms": 100, + "timeout": 5000, + "http_headers": { + "X-Custom-Header": "value" + } + } + }`, + Args: cobra.MinimumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + GetCredentials() // Validates and exits if not authenticated + + label := args[0] + configFile, _ := cmd.Flags().GetString("config") + + if configFile == "" { + return fmt.Errorf("--config flag is required to specify route configuration JSON file") + } + + // Get project UID (from config or --project flag) + projectUID := GetProject() + if projectUID == "" { + return fmt.Errorf("no project set. Use 'notehub project set ' or provide --project flag") + } + + // Read config file + configBytes, err := os.ReadFile(configFile) + if err != nil { + return fmt.Errorf("failed to read config file: %w", err) + } + + // Unmarshal into NotehubRoute struct + var routeConfig notehub.NotehubRoute + err = note.JSONUnmarshal(configBytes, &routeConfig) + if err != nil { + return fmt.Errorf("failed to parse config file: %w", err) + } + + // Override label if provided + routeConfig.Label = &label + + // Create route using SDK + client := GetNotehubClient() + ctx, err := GetNotehubContext() + if err != nil { + return err + } + + createdRoute, _, err := client.RouteAPI.CreateRoute(ctx, projectUID). + NotehubRoute(routeConfig). + Execute() + if err != nil { + return fmt.Errorf("failed to create route: %w", err) + } + + uid := "" + if createdRoute.Uid != nil { + uid = *createdRoute.Uid + } + routeType := "" + if createdRoute.Type != nil { + routeType = *createdRoute.Type + } + + fmt.Printf("\nRoute created successfully!\n\n") + fmt.Printf("UID: %s\n", uid) + fmt.Printf("Label: %s\n", label) + fmt.Printf("Type: %s\n", routeType) + fmt.Println() + + return nil + }, +} + +// routeUpdateCmd represents the route update command +var routeUpdateCmd = &cobra.Command{ + Use: "update [route-uid-or-name]", + Short: "Update an existing route", + Long: `Update an existing route by UID or name. + +Note: Route updates require a JSON configuration file. Use --config to specify the file. + +Examples: + # Update route from JSON file + notehub route update "My Route" --config route-update.json + + # Update by UID + notehub route update route:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx --config route-update.json + + # Example route-update.json (partial update): + { + "http": { + "url": "https://newexample.com/webhook", + "throttle_ms": 50 + } + }`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + GetCredentials() // Validates and exits if not authenticated + + routeIdentifier := args[0] + configFile, _ := cmd.Flags().GetString("config") + + if configFile == "" { + return fmt.Errorf("--config flag is required to specify route configuration JSON file") + } + + // Get project UID (from config or --project flag) + projectUID := GetProject() + if projectUID == "" { + return fmt.Errorf("no project set. Use 'notehub project set ' or provide --project flag") + } + + // Get SDK client + client := GetNotehubClient() + ctx, err := GetNotehubContext() + if err != nil { + return err + } + + // Determine route UID (handle both UID and name) + var routeUID string + + // First try as UID + if len(routeIdentifier) > 6 && routeIdentifier[:6] == "route:" { + routeUID = routeIdentifier + } else { + // Search by name + routes, _, err := client.RouteAPI.GetRoutes(ctx, projectUID).Execute() + if err != nil { + return fmt.Errorf("failed to list routes: %w", err) + } + + found := false + for _, route := range routes { + if route.Label != nil && *route.Label == routeIdentifier { + if route.Uid != nil { + routeUID = *route.Uid + found = true + break + } + } + } + + if !found { + return fmt.Errorf("route '%s' not found", routeIdentifier) + } + } + + // Read config file + configBytes, err := os.ReadFile(configFile) + if err != nil { + return fmt.Errorf("failed to read config file: %w", err) + } + + // Unmarshal into NotehubRoute struct + var routeConfig notehub.NotehubRoute + err = note.JSONUnmarshal(configBytes, &routeConfig) + if err != nil { + return fmt.Errorf("failed to parse config file: %w", err) + } + + // Update route using SDK + updatedRoute, _, err := client.RouteAPI.UpdateRoute(ctx, projectUID, routeUID). + NotehubRoute(routeConfig). + Execute() + if err != nil { + return fmt.Errorf("failed to update route: %w", err) + } + + label := "" + if updatedRoute.Label != nil { + label = *updatedRoute.Label + } + routeType := "" + if updatedRoute.Type != nil { + routeType = *updatedRoute.Type + } + + fmt.Printf("\nRoute updated successfully!\n\n") + fmt.Printf("UID: %s\n", routeUID) + fmt.Printf("Label: %s\n", label) + fmt.Printf("Type: %s\n", routeType) + fmt.Println() + + return nil + }, +} + +// routeDeleteCmd represents the route delete command +var routeDeleteCmd = &cobra.Command{ + Use: "delete [route-uid-or-name]", + Short: "Delete a route", + Long: `Delete a route by UID or name. + +Examples: + # Delete route by UID + notehub route delete route:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + + # Delete route by name + notehub route delete "My Route"`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + GetCredentials() // Validates and exits if not authenticated + + routeIdentifier := args[0] + + // Get project UID (from config or --project flag) + projectUID := GetProject() + if projectUID == "" { + return fmt.Errorf("no project set. Use 'notehub project set ' or provide --project flag") + } + + // Get SDK client + client := GetNotehubClient() + ctx, err := GetNotehubContext() + if err != nil { + return err + } + + // Determine route UID and name (handle both UID and name) + var routeUID string + var routeName string + + // First try as UID + if len(routeIdentifier) > 6 && routeIdentifier[:6] == "route:" { + routeUID = routeIdentifier + // Try to get route details for name + route, resp, err := client.RouteAPI.GetRoute(ctx, projectUID, routeUID).Execute() + if err == nil && resp != nil && resp.StatusCode != 404 && route.Label != nil { + routeName = *route.Label + } + } else { + // Search by name + routes, _, err := client.RouteAPI.GetRoutes(ctx, projectUID).Execute() + if err != nil { + return fmt.Errorf("failed to list routes: %w", err) + } + + found := false + for _, route := range routes { + if route.Label != nil && *route.Label == routeIdentifier { + if route.Uid != nil { + routeUID = *route.Uid + routeName = *route.Label + found = true + break + } + } + } + + if !found { + return fmt.Errorf("route '%s' not found", routeIdentifier) + } + } + + // Delete route using SDK + _, err = client.RouteAPI.DeleteRoute(ctx, projectUID, routeUID).Execute() + if err != nil { + return fmt.Errorf("failed to delete route: %w", err) + } + + fmt.Printf("\nRoute deleted successfully!\n\n") + fmt.Printf("UID: %s\n", routeUID) + if routeName != "" { + fmt.Printf("Label: %s\n", routeName) + } + fmt.Println() + + return nil + }, +} + +// routeLogsCmd represents the route logs command +var routeLogsCmd = &cobra.Command{ + Use: "logs [route-uid-or-name]", + Short: "Get logs for a specific route", + Long: `Get logs for a specific route by UID or name. + +Examples: + # Get logs for route + notehub route logs "My Route" + + # Get logs by UID + notehub route logs route:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + + # Get logs with pagination + notehub route logs "My Route" --page-size 100 --page-num 1 + + # Filter logs by device + notehub route logs "My Route" --device dev:864475046552567 + + # Get logs with JSON output + notehub route logs "My Route" --json`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + GetCredentials() // Validates and exits if not authenticated + + routeIdentifier := args[0] + + // Get project UID (from config or --project flag) + projectUID := GetProject() + if projectUID == "" { + return fmt.Errorf("no project set. Use 'notehub project set ' or provide --project flag") + } + + pageSize, _ := cmd.Flags().GetInt("page-size") + pageNum, _ := cmd.Flags().GetInt("page-num") + deviceUID, _ := cmd.Flags().GetString("device") + + // Get SDK client + client := GetNotehubClient() + ctx, err := GetNotehubContext() + if err != nil { + return err + } + + // Determine route UID (handle both UID and name) + var routeUID string + + // First try as UID + if len(routeIdentifier) > 6 && routeIdentifier[:6] == "route:" { + routeUID = routeIdentifier + } else { + // Search by name + routes, _, err := client.RouteAPI.GetRoutes(ctx, projectUID).Execute() + if err != nil { + return fmt.Errorf("failed to list routes: %w", err) + } + + found := false + for _, route := range routes { + if route.Label != nil && *route.Label == routeIdentifier { + if route.Uid != nil { + routeUID = *route.Uid + found = true + break + } + } + } + + if !found { + return fmt.Errorf("route '%s' not found", routeIdentifier) + } + } + + // Get route logs using SDK + req := client.RouteAPI.GetRouteLogsByRoute(ctx, projectUID, routeUID) + + if pageSize > 0 { + req = req.PageSize(int32(pageSize)) + } + if pageNum > 0 { + req = req.PageNum(int32(pageNum)) + } + if deviceUID != "" { + req = req.DeviceUID([]string{deviceUID}) + } + + logs, _, err := req.Execute() + if err != nil { + return fmt.Errorf("failed to get route logs: %w", err) + } + + // Handle JSON output + if GetJson() || GetPretty() { + var output []byte + var err error + if GetPretty() { + output, err = note.JSONMarshalIndent(logs, "", " ") + } else { + output, err = note.JSONMarshal(logs) + } + if err != nil { + return fmt.Errorf("failed to marshal JSON: %w", err) + } + fmt.Printf("%s\n", output) + return nil + } + + // Display logs in human-readable format + if len(logs) == 0 { + fmt.Println("No logs found.") + return nil + } + + fmt.Printf("\nRoute Logs (%d entries):\n", len(logs)) + fmt.Printf("========================\n\n") + + for i, entry := range logs { + fmt.Printf("%d. ", i+1) + + if entry.Date != nil { + fmt.Printf("%s\n", *entry.Date) + } else { + fmt.Println() + } + + if entry.EventUid != nil && *entry.EventUid != "" { + fmt.Printf(" Event UID: %s\n", *entry.EventUid) + } + if entry.Status != nil && *entry.Status != "" { + fmt.Printf(" Status: %s\n", *entry.Status) + } + if entry.Duration != nil { + fmt.Printf(" Duration: %d ms\n", *entry.Duration) + } + if entry.Url != nil && *entry.Url != "" { + fmt.Printf(" URL: %s\n", *entry.Url) + } + if entry.Text != nil && *entry.Text != "" { + fmt.Printf(" Response: %s\n", *entry.Text) + } + if entry.Attn != nil && *entry.Attn { + fmt.Printf(" ⚠ Attention Required\n") + } + fmt.Println() + } + + // Note: SDK returns a simple array, pagination info is in response headers + // For now, suggest using page-num to paginate + if len(logs) >= pageSize && pageSize > 0 { + fmt.Printf("Showing page %d (page size: %d). Use --page-num %d to see next page.\n", pageNum, pageSize, pageNum+1) + } + + return nil + }, +} + +func init() { + rootCmd.AddCommand(routeCmd) + routeCmd.AddCommand(routeListCmd) + routeCmd.AddCommand(routeGetCmd) + routeCmd.AddCommand(routeCreateCmd) + routeCmd.AddCommand(routeUpdateCmd) + routeCmd.AddCommand(routeDeleteCmd) + routeCmd.AddCommand(routeLogsCmd) + + // Add flags for route create + routeCreateCmd.Flags().String("config", "", "Path to JSON configuration file (required)") + routeCreateCmd.MarkFlagRequired("config") + + // Add flags for route update + routeUpdateCmd.Flags().String("config", "", "Path to JSON configuration file (required)") + routeUpdateCmd.MarkFlagRequired("config") + + // Add flags for route logs + routeLogsCmd.Flags().Int("page-size", 50, "Number of logs to return per page") + routeLogsCmd.Flags().Int("page-num", 1, "Page number to retrieve") + routeLogsCmd.Flags().String("device", "", "Filter logs by device UID") +} diff --git a/notehub/trace.go b/notehub/cmd/trace.go similarity index 67% rename from notehub/trace.go rename to notehub/cmd/trace.go index 67d85bf..36c120b 100644 --- a/notehub/trace.go +++ b/notehub/cmd/trace.go @@ -1,8 +1,8 @@ -// Copyright 2021 Blues Inc. All rights reserved. +// Copyright 2025 Blues Inc. All rights reserved. // Use of this source code is governed by licenses granted by the // copyright holder including that found in the LICENSE file. -package main +package cmd import ( "bufio" @@ -11,9 +11,40 @@ import ( "regexp" "strings" - "github.com/blues/note-cli/lib" + "github.com/spf13/cobra" ) +// traceCmd represents the trace command +var traceCmd = &cobra.Command{ + Use: "trace", + Short: "Enter interactive trace mode", + Long: `Enter an interactive trace mode to send requests to Notehub. + +In trace mode, you can: + - Set project, product, and device context + - Send JSON requests + - Make HTTPS GET, POST, PUT, DELETE requests + - Ping the Notehub + - Type ? for help + +Example: + notehub trace --project app:xxxx --device dev:yyyy`, + RunE: func(cmd *cobra.Command, args []string) error { + GetCredentials() // Validate credentials + + // Set initial context + reqFlagApp = GetProject() + reqFlagProduct = GetProduct() + reqFlagDevice = GetDevice() + + return traceMode() + }, +} + +func init() { + rootCmd.AddCommand(traceCmd) +} + // Command definitions type cmdDef struct { Command string @@ -21,7 +52,8 @@ type cmdDef struct { } func validCommands() []cmdDef { - return []cmdDef{{"product", "set productUID for requests made in this session"}, + return []cmdDef{ + {"product", "set productUID for requests made in this session"}, {"project", "set projectUID (appUID) for requests made in this session"}, {"device", "set deviceUID for requests made in this session"}, {"hub", "set notehub domain for requests made in this session"}, @@ -35,8 +67,7 @@ func validCommands() []cmdDef { } // Enter a diagnostic trace mode -func trace() error { - +func traceMode() error { // Create a scanner to watch stdin scanner := bufio.NewScanner(os.Stdin) var cmd string @@ -58,7 +89,7 @@ traceloop: // Process JSON requests if strings.HasPrefix(cmd, "{") { - _, err := reqHubV0JSON(true, lib.ConfigAPIHub(), []byte(cmd), "", "", "", "", false, false, nil) + _, err := reqHubV0JSON(true, GetAPIHub(), []byte(cmd), "", "", "", "", false, false, nil) if err != nil { fmt.Printf("error: %s\n", err) } @@ -66,21 +97,21 @@ traceloop: } // Create clean IDs to work with in the commands - cleanProduct := flagProduct + cleanProduct := reqFlagProduct if cleanProduct != "" && !strings.HasPrefix(cleanProduct, "product:") { - cleanProduct = "product:" + flagProduct + cleanProduct = "product:" + reqFlagProduct } - cleanApp := flagApp + cleanApp := reqFlagApp if !strings.HasPrefix(cleanApp, "app:") { if cleanApp == "" { cleanApp = cleanProduct } else { - cleanApp = "app:" + flagApp + cleanApp = "app:" + reqFlagApp } } - cleanDevice := flagDevice + cleanDevice := reqFlagDevice if !strings.HasPrefix(cleanDevice, "dev:") { - cleanDevice = "dev:" + flagDevice + cleanDevice = "dev:" + reqFlagDevice } cmdAfter0 = strings.Replace(cmdAfter0, "{productUID}", cleanProduct, -1) cmdAfter0 = strings.Replace(cmdAfter0, "{projectUID}", cleanApp, -1) @@ -99,9 +130,9 @@ traceloop: if args[1] == "-" { args[1] = "" } - flagProduct = args[1] + reqFlagProduct = args[1] } - fmt.Printf("productUID is %s\n", flagProduct) + fmt.Printf("productUID is %s\n", reqFlagProduct) case "project": fallthrough @@ -110,28 +141,28 @@ traceloop: if args[1] == "-" { args[1] = "" } - flagApp = args[1] + reqFlagApp = args[1] } - fmt.Printf("projectUID is %s\n", flagApp) + fmt.Printf("projectUID is %s\n", reqFlagApp) case "device": if args[1] != "" { if args[1] == "-" { args[1] = "" } - flagDevice = args[1] + reqFlagDevice = args[1] } - fmt.Printf("deviceUID is %s\n", flagDevice) + fmt.Printf("deviceUID is %s\n", reqFlagDevice) case "hub": if args[1] != "" { if args[1] == "-" { args[1] = "" } - config, _ := lib.GetConfig() - config.Hub = args[1] + SetHub(args[1]) + SaveConfig() } - fmt.Printf("hub is %s\n", flagApp) + fmt.Printf("hub is %s\n", GetHub()) case "get": fallthrough @@ -158,20 +189,20 @@ traceloop: } // Perform the transaction - _, err := reqHubV1JSON(true, lib.ConfigAPIHub(), args[0], url, bodyJSON) + _, err := reqHubV1JSON(true, GetAPIHub(), args[0], url, bodyJSON) if err != nil { fmt.Printf("error: %s\n", err) return err } case "ping": - _, err := reqHubV1JSON(true, lib.ConfigAPIHub(), "GET", "/ping", nil) + _, err := reqHubV1JSON(true, GetAPIHub(), "GET", "/ping", nil) if err != nil { fmt.Printf("error: %s\n", err) return err } if cleanApp != "" { url := "/v1/products/" + cleanApp + "/products" - _, err = reqHubV1JSON(true, lib.ConfigAPIHub(), "GET", url, nil) + _, err = reqHubV1JSON(true, GetAPIHub(), "GET", url, nil) if err != nil { fmt.Printf("error: %s\n", err) return err diff --git a/notehub/cmd/upload.go b/notehub/cmd/upload.go new file mode 100644 index 0000000..458fd78 --- /dev/null +++ b/notehub/cmd/upload.go @@ -0,0 +1,168 @@ +// Copyright 2025 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package cmd + +import ( + "bytes" + "fmt" + "io" + "mime/multipart" + "net/http" + "os" + "path/filepath" + + "github.com/spf13/cobra" +) + +var ( + flagType string + flagTags string + flagNotes string + flagOverwrite bool +) + +// uploadCmd represents the upload command +var uploadCmd = &cobra.Command{ + Use: "upload [file]", + Short: "Upload firmware to Notehub", + Long: `Upload host or notecard firmware to Notehub using the V1 API. + +The firmware type must be specified as either 'host' or 'notecard' using the --type flag. + +Example: + notehub upload firmware.bin --type host --project app:xxxx + notehub upload notecard-fw.bin --type notecard --product com.company:product --notes "Bug fixes"`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + GetCredentials() // Validate credentials + + filename := args[0] + + // Determine project UID + projectUID := GetProject() + if projectUID == "" { + product := GetProduct() + if product != "" { + projectUID = product + } + } + if projectUID == "" { + return fmt.Errorf("--project or --product flag is required") + } + + // Validate firmware type + if flagType == "" { + return fmt.Errorf("--type flag is required (must be 'host' or 'notecard')") + } + if flagType != "host" && flagType != "notecard" { + return fmt.Errorf("--type must be either 'host' or 'notecard', got: %s", flagType) + } + + // Upload using V1 API + err := uploadFirmwareV1(projectUID, flagType, filename, flagNotes, GetVerbose()) + if err != nil { + return err + } + + fmt.Printf("Successfully uploaded %s firmware: %s\n", flagType, filename) + return nil + }, +} + +func init() { + rootCmd.AddCommand(uploadCmd) + + uploadCmd.Flags().StringVar(&flagType, "type", "", "Firmware type: 'host' or 'notecard' (required)") + uploadCmd.Flags().StringVar(&flagNotes, "notes", "", "Notes describing the firmware") + uploadCmd.MarkFlagRequired("type") +} + +// uploadFirmwareV1 uploads firmware using the V1 API +// PUT /v1/projects/{projectOrProductUID}/firmware/{firmwareType}/{filename} +func uploadFirmwareV1(projectUID, firmwareType, filename, notes string, verbose bool) error { + // Read the file + file, err := os.Open(filename) + if err != nil { + return fmt.Errorf("failed to open file: %w", err) + } + defer file.Close() + + // Get file info + fileInfo, err := file.Stat() + if err != nil { + return fmt.Errorf("failed to stat file: %w", err) + } + + // Create multipart form data + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + // Add file + part, err := writer.CreateFormFile("file", filepath.Base(filename)) + if err != nil { + return fmt.Errorf("failed to create form file: %w", err) + } + + _, err = io.Copy(part, file) + if err != nil { + return fmt.Errorf("failed to copy file: %w", err) + } + + // Close the writer to set the terminating boundary + err = writer.Close() + if err != nil { + return fmt.Errorf("failed to close writer: %w", err) + } + + // Build URL + hub := GetAPIHub() + url := fmt.Sprintf("https://%s/v1/projects/%s/firmware/%s/%s", + hub, projectUID, firmwareType, filepath.Base(filename)) + + // Add query parameters if provided + if notes != "" { + url += fmt.Sprintf("?notes=%s", notes) + } + + // Create HTTP request + req, err := http.NewRequest("PUT", url, body) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + // Set headers + req.Header.Set("Content-Type", writer.FormDataContentType()) + req.Header.Set("User-Agent", "notehub-client") + + // Add authentication + err = AddAuthenticationHeader(req) + if err != nil { + return fmt.Errorf("failed to set auth header: %w", err) + } + + if verbose { + fmt.Printf("PUT %s (file size: %d bytes)\n", url, fileInfo.Size()) + } + + // Send request + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("failed to upload: %w", err) + } + defer resp.Body.Close() + + // Check response + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + responseBody, _ := io.ReadAll(resp.Body) + return fmt.Errorf("upload failed with status %d: %s", resp.StatusCode, string(responseBody)) + } + + if verbose { + fmt.Printf("Upload successful (status: %d)\n", resp.StatusCode) + } + + return nil +} diff --git a/notehub/cmd/vars.go b/notehub/cmd/vars.go new file mode 100644 index 0000000..4d229d2 --- /dev/null +++ b/notehub/cmd/vars.go @@ -0,0 +1,163 @@ +// Copyright 2025 Blues Inc. All rights reserved. +// Use of this source code is governed by licenses granted by the +// copyright holder including that found in the LICENSE file. + +package cmd + +import ( + "fmt" + "os" + "strings" + + "github.com/blues/note-go/note" + "github.com/spf13/cobra" +) + +var ( + flagScope string + flagSn string +) + +// varsCmd represents the vars command +var varsCmd = &cobra.Command{ + Use: "vars", + Short: "Manage environment variables", + Long: `Commands for getting and setting environment variables for devices and fleets.`, +} + +// varsGetCmd represents the vars get command +var varsGetCmd = &cobra.Command{ + Use: "get", + Short: "Get environment variables", + Long: `Get environment variables for devices or fleets. + +Scope can be: + - A device UID (dev:xxxx) + - A fleet UID (fleet:xxxx) + - A fleet name or pattern (e.g., "production" or "prod*") + - @fleetname to get all devices in a fleet + - @ to get all devices in the project + - A comma-separated list of any of the above + - @filename to read scope from a file`, + RunE: func(cmd *cobra.Command, args []string) error { + GetCredentials() // Validate credentials + + if flagScope == "" { + return fmt.Errorf("use --scope to specify device(s) or fleet(s)") + } + + appMetadata, scopeDevices, scopeFleets, err := ResolveScopeWithValidation(flagScope) + if err != nil { + return err + } + + if len(scopeDevices) != 0 && len(scopeFleets) != 0 { + return fmt.Errorf("scope may include devices or fleets but not both") + } + + verbose := GetVerbose() + var vars map[string]Vars + if len(scopeDevices) != 0 { + vars, err = varsGetFromDevices(appMetadata, scopeDevices, verbose) + } else if len(scopeFleets) != 0 { + vars, err = varsGetFromFleets(appMetadata, scopeFleets, verbose) + } + if err != nil { + return err + } + + var varsJSON []byte + if GetPretty() { + varsJSON, err = note.JSONMarshalIndent(vars, "", " ") + } else { + varsJSON, err = note.JSONMarshal(vars) + } + if err != nil { + return err + } + + fmt.Printf("%s\n", varsJSON) + return nil + }, +} + +// varsSetCmd represents the vars set command +var varsSetCmd = &cobra.Command{ + Use: "set [json]", + Short: "Set environment variables", + Long: `Set environment variables for devices or fleets. + +The JSON argument can be a JSON object or @filename to read from a file. + +Example: + notehub vars set --scope dev:xxxx '{"VAR1":"value1","VAR2":"value2"}' + notehub vars set --scope @production @vars.json`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + GetCredentials() // Validate credentials + + if flagScope == "" { + return fmt.Errorf("use --scope to specify device(s) or fleet(s)") + } + + appMetadata, scopeDevices, scopeFleets, err := ResolveScopeWithValidation(flagScope) + if err != nil { + return err + } + + if len(scopeDevices) != 0 && len(scopeFleets) != 0 { + return fmt.Errorf("scope may include devices or fleets but not both") + } + + // Parse template + template := Vars{} + varsArg := args[0] + if strings.HasPrefix(varsArg, "@") { + var templateJSON []byte + templateJSON, err = os.ReadFile(strings.TrimPrefix(varsArg, "@")) + if err != nil { + return err + } + err = note.JSONUnmarshal(templateJSON, &template) + } else { + err = note.JSONUnmarshal([]byte(varsArg), &template) + } + if err != nil { + return err + } + + verbose := GetVerbose() + var vars map[string]Vars + if len(scopeDevices) != 0 { + vars, err = varsSetFromDevices(appMetadata, scopeDevices, template, verbose) + } else if len(scopeFleets) != 0 { + vars, err = varsSetFromFleets(appMetadata, scopeFleets, template, verbose) + } + if err != nil { + return err + } + + var varsJSON []byte + if GetPretty() { + varsJSON, err = note.JSONMarshalIndent(vars, "", " ") + } else { + varsJSON, err = note.JSONMarshal(vars) + } + if err != nil { + return err + } + + fmt.Printf("%s\n", varsJSON) + return nil + }, +} + +func init() { + rootCmd.AddCommand(varsCmd) + varsCmd.AddCommand(varsGetCmd) + varsCmd.AddCommand(varsSetCmd) + + // Flags for vars commands + varsGetCmd.Flags().StringVarP(&flagScope, "scope", "s", "", "Device/fleet scope (required)") + varsSetCmd.Flags().StringVarP(&flagScope, "scope", "s", "", "Device/fleet scope (required)") +} diff --git a/notehub/doc/CLI.md b/notehub/doc/CLI.md new file mode 100644 index 0000000..11e8704 --- /dev/null +++ b/notehub/doc/CLI.md @@ -0,0 +1,914 @@ +# Notehub CLI Documentation + +The Notehub CLI is a command-line tool for interacting with Blues Notehub. It provides commands for authentication, managing projects, devices, fleets, products, and firmware updates. + +## Table of Contents + +- [Global Flags](#global-flags) +- [Device Scoping](#device-scoping) +- [Authentication](#authentication) +- [Project Management](#project-management) +- [Product Management](#product-management) +- [Fleet Management](#fleet-management) +- [Route Management](#route-management) +- [Device Management](#device-management) +- [Firmware Updates (DFU)](#firmware-updates-dfu) +- [Configuration](#configuration) +- [Examples](#examples) +- [Tips](#tips) +- [Error Handling](#error-handling) +- [Getting Help](#getting-help) + +--- + +## Global Flags + +These flags are available for all commands: + +| Flag | Short | Description | +| ----------- | ----- | --------------------------------------- | +| `--project` | `-p` | Project UID to use for the command | +| `--product` | | Product UID to use for the command | +| `--device` | `-d` | Device UID to use for the command | +| `--verbose` | `-v` | Display requests and responses | +| `--json` | | Output only JSON (strip non-JSON lines) | +| `--pretty` | | Pretty print JSON output | + +--- + +## Device Scoping + +Many commands support flexible device scoping to target one or more devices. The scope syntax is consistent across all device and firmware update commands. + +### Scope Formats + +| Format | Description | Example | +|--------|-------------|---------| +| `dev:xxxx` | Single device by UID | `dev:864475046552567` | +| `imei:xxxx` | Single device by IMEI | `imei:123456789012345` | +| `fleet:xxxx` | All devices in fleet (by UID) | `fleet:abc123...` | +| `production` | All devices in named fleet | `production` | +| `prod*` | Fleet wildcard matching | `prod*` (matches prod-east, prod-west) | +| `@fleet-name` | Fleet indirection (explicit) | `@production` | +| `@` | All devices in project | `@` | +| `@devices.txt` | Device UIDs from file (one per line) | `@devices.txt` | +| `dev:a,dev:b` | Multiple scopes (comma-separated) | `dev:111,dev:222` | + +### Fleet Name vs Fleet Indirection + +Both `production` and `@production` target all devices in the "production" fleet: + +- **Fleet name** (`production`) - Direct, supports wildcards (`prod*`) +- **Fleet indirection** (`@production`) - Explicit, clearer intent + +Use whichever style you prefer - they work identically for single fleet names. + +### File-Based Scoping + +Create a text file with device UIDs (one per line): + +``` +dev:864475046552567 +dev:864475046552568 +dev:864475046552569 +``` + +Then reference it with `@devices.txt` in any command that accepts scope. + +--- + +## Authentication + +Commands for signing in, signing out, and managing authentication tokens. + +### `notehub auth signin` + +Sign in to Notehub using browser-based OAuth2 flow. + +```bash +# Basic signin +notehub auth signin + +# Sign in and automatically set a project +notehub auth signin --set-project "My Project" +notehub auth signin --set-project app:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +``` + +**Flags:** +- `--set-project`: Automatically set project after signin (name or UID) + +### `notehub auth signin-token [token]` + +Sign in to Notehub using a personal access token. + +```bash +# Sign in with a personal access token +notehub auth signin-token your-personal-access-token + +# Sign in with token and set project +notehub auth signin-token your-token --set-project "My Project" +``` + +**Flags:** +- `--set-project`: Automatically set project after signin (name or UID) + +### `notehub auth signout` + +Sign out of Notehub and remove stored credentials. + +```bash +notehub auth signout +``` + +### `notehub auth token` + +Display the current authentication token. + +```bash +notehub auth token +``` + +--- + +## Project Management + +Commands for listing and selecting Notehub projects. + +### `notehub project list` + +List all projects for the authenticated user. + +```bash +# List all projects +notehub project list + +# List with JSON output +notehub project list --json + +# List with pretty JSON +notehub project list --pretty +``` + +### `notehub project get [project-name-or-uid]` + +Get detailed information about a specific project. If no project is specified, uses the active project. + +```bash +# Get information about active project +notehub project get + +# Get information about specific project by UID +notehub project get app:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + +# Get information about specific project by name +notehub project get "My Project" + +# Get with JSON output +notehub project get --json + +# Get with pretty JSON +notehub project get --pretty +``` + +### `notehub project set [project-name-or-uid]` + +Set the active project in the configuration. + +```bash +# Set project by name +notehub project set "My Project" + +# Set project by UID +notehub project set app:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +``` + +### `notehub project clear` + +Clear the active project from the configuration. + +```bash +notehub project clear +``` + +--- + +## Product Management + +Commands for listing and managing products in Notehub projects. + +### `notehub product list` + +List all products in the current or specified project. + +```bash +# List products in current project +notehub product list + +# List products in specific project +notehub product list --project app:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + +# List with JSON output +notehub product list --json + +# List with pretty JSON +notehub product list --pretty +``` + +### `notehub product get [product-uid-or-name]` + +Get detailed information about a specific product. + +```bash +# Get product by name +notehub product get "My Product" + +# Get product by UID +notehub product get com.company.user:product-name + +# Get with JSON output +notehub product get "My Product" --json +``` + +--- + +## Fleet Management + +Commands for listing and managing fleets in Notehub projects. + +### `notehub fleet list` + +List all fleets in the current or specified project. + +```bash +# List all fleets +notehub fleet list + +# List fleets in specific project +notehub fleet list --project app:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + +# List with JSON output +notehub fleet list --json +``` + +### `notehub fleet get [fleet-uid-or-name]` + +Get detailed information about a specific fleet. + +```bash +# Get fleet by name +notehub fleet get production + +# Get fleet by UID +notehub fleet get fleet:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + +# Get with pretty JSON +notehub fleet get production --pretty +``` + +### `notehub fleet create [name]` + +Create a new fleet in the current project. + +```bash +# Create a simple fleet +notehub fleet create production + +# Create fleet with smart rule +notehub fleet create production --smart-rule '$contains(environment_variables.stage, "prod")' + +# Create fleet with connectivity assurance enabled +notehub fleet create production --connectivity-assurance +``` + +**Flags:** +- `--smart-rule`: JSONata expression for dynamic fleet membership +- `--connectivity-assurance`: Enable connectivity assurance for this fleet + +### `notehub fleet update [fleet-uid-or-name]` + +Update a fleet's properties. + +```bash +# Update fleet name +notehub fleet update production --name production-fleet + +# Update smart rule +notehub fleet update production --smart-rule '$contains(tags, "prod")' + +# Enable connectivity assurance +notehub fleet update production --connectivity-assurance true + +# Set watchdog timer +notehub fleet update production --watchdog-mins 60 +``` + +**Flags:** +- `--name`: New name for the fleet +- `--smart-rule`: JSONata expression for dynamic fleet membership +- `--connectivity-assurance`: Enable or disable connectivity assurance +- `--watchdog-mins`: Watchdog timer in minutes (0 to disable) + +### `notehub fleet delete [fleet-uid-or-name]` + +Delete a fleet from the current project. + +```bash +# Delete fleet by name +notehub fleet delete production + +# Delete fleet by UID +notehub fleet delete fleet:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +``` + +--- + +## Route Management + +Commands for creating, updating, deleting, and viewing routes in Notehub projects. + +### `notehub route list` + +List all routes in the current or specified project. + +```bash +# List all routes +notehub route list + +# List routes in specific project +notehub route list --project app:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + +# List with JSON output +notehub route list --json + +# List with pretty JSON +notehub route list --pretty +``` + +### `notehub route get [route-uid-or-name]` + +Get detailed information about a specific route. + +```bash +# Get route by UID +notehub route get route:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + +# Get route by name +notehub route get "My Route" + +# Get with pretty JSON +notehub route get "My Route" --pretty +``` + +### `notehub route create [label]` + +Create a new route in the current project. Requires a JSON configuration file. + +```bash +# Create route from JSON file +notehub route create "My Route" --config route.json +``` + +**Example route.json for HTTP route:** + +```json +{ + "label": "My HTTP Route", + "http": { + "url": "https://example.com/webhook", + "fleets": ["fleet:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"], + "throttle_ms": 100, + "timeout": 5000, + "http_headers": { + "X-Custom-Header": "value" + } + } +} +``` + +**Flags:** + +- `--config`: Path to JSON configuration file (required) + +### `notehub route update [route-uid-or-name]` + +Update an existing route. Requires a JSON configuration file. + +```bash +# Update route from JSON file +notehub route update "My Route" --config route-update.json + +# Update by UID +notehub route update route:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx --config route-update.json +``` + +**Example route-update.json (partial update):** + +```json +{ + "http": { + "url": "https://newexample.com/webhook", + "throttle_ms": 50 + } +} +``` + +**Flags:** + +- `--config`: Path to JSON configuration file (required) + +### `notehub route delete [route-uid-or-name]` + +Delete a route from the current project. + +```bash +# Delete route by UID +notehub route delete route:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + +# Delete route by name +notehub route delete "My Route" +``` + +### `notehub route logs [route-uid-or-name]` + +Get logs for a specific route, showing delivery attempts, errors, and status information. + +```bash +# Get logs for route +notehub route logs "My Route" + +# Get logs by UID +notehub route logs route:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + +# Get logs with pagination +notehub route logs "My Route" --page-size 100 --page-num 1 + +# Filter logs by device +notehub route logs "My Route" --device dev:864475046552567 + +# Get logs with JSON output +notehub route logs "My Route" --json +``` + +**Flags:** + +- `--page-size`: Number of logs to return per page (default: 50) +- `--page-num`: Page number to retrieve (default: 1) +- `--device`: Filter logs by device UID + +--- + +## Device Management + +Commands for listing and managing devices in Notehub projects. + +### `notehub device list` + +List all devices in the current or specified project. + +```bash +# List all devices +notehub device list + +# List devices in specific project +notehub device list --project app:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx + +# List with JSON output +notehub device list --json +``` + +### `notehub device enable [scope]` + +Enable one or more devices, allowing them to communicate with Notehub. + +See [Device Scoping](#device-scoping) for all supported scope formats. + +```bash +# Enable a single device +notehub device enable dev:864475046552567 + +# Enable all devices in a fleet (by name) +notehub device enable production + +# Enable all devices in a fleet (explicit indirection) +notehub device enable @production + +# Enable all devices in project +notehub device enable @ + +# Enable devices from a file +notehub device enable @devices.txt + +# Enable multiple devices +notehub device enable dev:111,dev:222,dev:333 + +# Enable devices with wildcard fleet matching +notehub device enable prod* +``` + +### `notehub device disable [scope]` + +Disable one or more devices, preventing them from communicating with Notehub. + +See [Device Scoping](#device-scoping) for all supported scope formats. + +```bash +# Disable a single device +notehub device disable dev:864475046552567 + +# Disable all devices in a fleet +notehub device disable @production + +# Disable all devices in project +notehub device disable @ + +# Disable devices from a file +notehub device disable @devices.txt +``` + +### `notehub device move [scope] [fleet-uid-or-name]` + +Move one or more devices to a fleet. + +See [Device Scoping](#device-scoping) for all supported scope formats. + +```bash +# Move a single device to a fleet +notehub device move dev:864475046552567 production + +# Move a device to a fleet by UID +notehub device move dev:864475046552567 fleet:xxxx + +# Move all devices from one fleet to another +notehub device move @old-fleet new-fleet + +# Move devices from a file to a fleet +notehub device move @devices.txt production + +# Move multiple devices to a fleet +notehub device move dev:111,dev:222 production +``` + +### `notehub device health [device-uid]` + +Get the health log for a specific device, showing boot events, DFU completions, and other health-related information. + +```bash +# Get health log for a device +notehub device health dev:864475046552567 + +# Get health log with JSON output +notehub device health dev:864475046552567 --json + +# Get health log with pretty JSON +notehub device health dev:864475046552567 --pretty +``` + +### `notehub device session [device-uid]` + +Get the session log for a specific device, showing connection history, network information, and session statistics. + +```bash +# Get session log for a device +notehub device session dev:864475046552567 + +# Get session log with JSON output +notehub device session dev:864475046552567 --json + +# Get session log with pretty JSON +notehub device session dev:864475046552567 --pretty +``` + +--- + +## Firmware Updates (DFU) + +Commands for scheduling and managing firmware updates for Notecards and host MCUs. + +### `notehub dfu list` + +List all firmware files available in the current project. + +```bash +# List all firmware files +notehub dfu list + +# List only host firmware +notehub dfu list --type host + +# List only notecard firmware +notehub dfu list --type notecard + +# Filter by version +notehub dfu list --version "1.2.3" + +# Filter by target +notehub dfu list --target "stm32" + +# List with pretty JSON +notehub dfu list --pretty +``` + +**Flags:** +- `--type`: Filter by firmware type (host or notecard) +- `--product`: Filter by product UID +- `--version`: Filter by version +- `--target`: Filter by target device +- `--filename`: Filter by filename + +### `notehub dfu update [firmware-type] [filename] [scope]` + +Schedule a firmware update for devices. Firmware type must be either `host` or `notecard`. + +See [Device Scoping](#device-scoping) for all supported scope formats. + +**Additional Filter Flags:** + +These filters narrow down the scope further: + +- `--tag`: Filter by device tags (comma-separated) +- `--serial`: Filter by serial numbers (comma-separated) +- `--location`: Filter by location +- `--notecard-firmware`: Filter by Notecard firmware version +- `--host-firmware`: Filter by host firmware version +- `--product`: Filter by product UID +- `--sku`: Filter by SKU + +```bash +# Schedule notecard firmware update for a specific device +notehub dfu update notecard notecard-6.2.1.bin dev:864475046552567 + +# Schedule host firmware update for all devices in a fleet +notehub dfu update host app-v1.2.3.bin @production + +# Schedule update for multiple devices +notehub dfu update notecard notecard-6.2.1.bin dev:111,dev:222,dev:333 + +# Schedule update for all devices in project +notehub dfu update notecard notecard-6.2.1.bin @ + +# Schedule update for devices from a file +notehub dfu update host app-v1.2.3.bin @devices.txt + +# Schedule update for fleet with wildcard +notehub dfu update notecard notecard-6.2.1.bin prod* + +# Schedule update with additional SKU filter +notehub dfu update notecard notecard-6.2.1.bin @production --sku NOTE-WBEX + +# Schedule update with additional location filter +notehub dfu update host app-v1.2.3.bin @production --location "San Francisco" + +# Combine scope with multiple additional filters +notehub dfu update notecard notecard-6.2.1.bin @production --tag outdoor --sku NOTE-WBEX +``` + +### `notehub dfu cancel [firmware-type] [scope]` + +Cancel pending firmware updates for devices. Firmware type must be either `host` or `notecard`. + +See [Device Scoping](#device-scoping) for all supported scope formats. + +**Additional Filter Flags:** + +- `--tag`: Filter by device tags (comma-separated) +- `--serial`: Filter by serial numbers (comma-separated) + +```bash +# Cancel notecard firmware update for a specific device +notehub dfu cancel notecard dev:864475046552567 + +# Cancel host firmware updates for all devices in a fleet +notehub dfu cancel host @production + +# Cancel updates for multiple devices +notehub dfu cancel notecard dev:111,dev:222,dev:333 + +# Cancel updates for all devices in project +notehub dfu cancel notecard @ + +# Cancel updates for devices from a file +notehub dfu cancel host @devices.txt + +# Cancel updates with additional tag filter +notehub dfu cancel host @production --tag outdoor +``` + +--- + +## Configuration + +The CLI stores configuration in `~/.notehub/config.yaml`, including: + +- Active project UID +- API hub URL (default: notehub.io) +- Authentication credentials (stored securely) + +You can also use environment variables: + +```bash +export NOTEHUB_PROJECT=app:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +export NOTEHUB_VERBOSE=true +``` + +--- + +## Examples + +### Common Workflows + +**1. Initial Setup** + +```bash +# Sign in to Notehub +notehub auth signin + +# List available projects +notehub project list + +# Set active project +notehub project set "My Project" + +# List devices in project +notehub device list +``` + +**2. Managing a Fleet** + +```bash +# Create a production fleet +notehub fleet create production + +# Move devices to the fleet +notehub device move dev:123456 production +notehub device move dev:789012 production + +# Or move multiple devices at once +notehub device move dev:123456,dev:789012 production + +# View fleet details +notehub fleet get production + +# Enable all devices in the fleet +notehub device enable @production +``` + +**3. Firmware Update Workflow** + +```bash +# List available firmware +notehub dfu list --type notecard + +# Schedule firmware update for a fleet +notehub dfu update notecard notecard-6.2.1.bin @production + +# Schedule update with additional filtering +notehub dfu update notecard notecard-6.2.1.bin @production --sku NOTE-WBEX + +# Check device health after update +notehub device health dev:864475046552567 + +# Cancel pending updates if needed +notehub dfu cancel notecard @production +``` + +**4. Device Troubleshooting** + +```bash +# Check device health +notehub device health dev:864475046552567 + +# View session history +notehub device session dev:864475046552567 + +# Get verbose output for debugging +notehub device session dev:864475046552567 --verbose +``` + +**5. Using File-Based Scoping** + +```bash +# Create a file with device UIDs +cat > devices.txt <