andon-client 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- andon_client-1.0.0/.gitignore +5 -0
- andon_client-1.0.0/CHANGELOG.md +18 -0
- andon_client-1.0.0/LICENSE +21 -0
- andon_client-1.0.0/PKG-INFO +195 -0
- andon_client-1.0.0/README.md +170 -0
- andon_client-1.0.0/pyproject.toml +42 -0
- andon_client-1.0.0/src/andon_client/__init__.py +25 -0
- andon_client-1.0.0/src/andon_client/_accepts.py +222 -0
- andon_client-1.0.0/src/andon_client/_env.py +73 -0
- andon_client-1.0.0/src/andon_client/_js.py +191 -0
- andon_client-1.0.0/src/andon_client/_relay.py +157 -0
- andon_client-1.0.0/src/andon_client/_runtime.py +409 -0
- andon_client-1.0.0/src/andon_client/_seal.py +110 -0
- andon_client-1.0.0/src/andon_client/_tiles.py +419 -0
- andon_client-1.0.0/src/andon_client/_validate.py +616 -0
- andon_client-1.0.0/src/andon_client/_view.py +252 -0
- andon_client-1.0.0/src/andon_client/_words.py +51 -0
- andon_client-1.0.0/src/andon_client/py.typed +0 -0
- andon_client-1.0.0/src/andon_client/schema/view-v1.schema.json +732 -0
- andon_client-1.0.0/tests/data/view-v1.sample.json +1 -0
- andon_client-1.0.0/tests/helpers.py +83 -0
- andon_client-1.0.0/tests/test_runtime.py +230 -0
- andon_client-1.0.0/tests/test_seal.py +60 -0
- andon_client-1.0.0/tests/test_thread.py +191 -0
- andon_client-1.0.0/tests/test_tiles.py +123 -0
- andon_client-1.0.0/tests/test_validate.py +104 -0
- andon_client-1.0.0/tests/test_view.py +248 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Versions follow [semantic versioning](https://semver.org): a major version may
|
|
4
|
+
break existing code, and its entry says what to change.
|
|
5
|
+
|
|
6
|
+
## 1.0.0
|
|
7
|
+
|
|
8
|
+
The first version.
|
|
9
|
+
|
|
10
|
+
- **AndonView**: `set(tile_id, value, at=, series=, lane=)`, `set({tile_id: value})`,
|
|
11
|
+
`replace(document)`, `flush()`, `close()`, `with`, `AndonView.from_env()` with
|
|
12
|
+
`env_file` for the configurator's `.env`, `on_sent` and `on_error`.
|
|
13
|
+
- **The rules of `@smartrsolutions/andon-client` 1.0.1**, the package for Node.js:
|
|
14
|
+
what a value does to its tile, the checks and the words of every refusal, the
|
|
15
|
+
sealing, and when an upload goes out. Its answers to some 2,900 documents and
|
|
16
|
+
values are part of this package's tests.
|
|
17
|
+
- Uploads run in a thread of the package's own, which lives while an upload is
|
|
18
|
+
due; a script that sets its values and ends still sends them.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SMARTR.solutions
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: andon-client
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Send values from Python to the Andon app for iPhone and iPad: end-to-end encrypted tiles and widgets.
|
|
5
|
+
Project-URL: Homepage, https://andon.app/en/developers
|
|
6
|
+
Project-URL: Source, https://github.com/smartrsolutions/andon-client-python
|
|
7
|
+
Project-URL: Changelog, https://github.com/smartrsolutions/andon-client-python/blob/main/CHANGELOG.md
|
|
8
|
+
Project-URL: Issues, https://github.com/smartrsolutions/andon-client-python/issues
|
|
9
|
+
Author-email: "SMARTR.solutions" <support@andon.app>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: andon,dashboard,iot,ipad,iphone,widget
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Intended Audience :: Manufacturing
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Topic :: System :: Monitoring
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Requires-Dist: cryptography>=3.4
|
|
23
|
+
Requires-Dist: jsonschema>=4.18
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# Andon client for Python
|
|
27
|
+
|
|
28
|
+
Send values from Python to [Andon](https://andon.app), the app for iPhone and iPad
|
|
29
|
+
that shows your data as tiles and widgets. Everything is end-to-end encrypted: the
|
|
30
|
+
content key stays in your process, and only the devices you paired can read what
|
|
31
|
+
you send.
|
|
32
|
+
|
|
33
|
+
**Get the app:** [Andon on the App Store](https://apps.apple.com/app/id6819064723).
|
|
34
|
+
|
|
35
|
+
## Getting started
|
|
36
|
+
|
|
37
|
+
1. **Design** your tiles in the [Andon configurator](https://andon.app/en/configurator)
|
|
38
|
+
and create the view there. Download the `.env` and the document, `view.json`:
|
|
39
|
+
it is the template your values fill.
|
|
40
|
+
2. **Pair** your iPhone or iPad: scan the configurator's QR code with the
|
|
41
|
+
[Andon app](https://apps.apple.com/app/id6819064723).
|
|
42
|
+
3. **Install** this package: `pip install andon-client`.
|
|
43
|
+
4. **Send values**, by tile ID:
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from datetime import datetime, timezone
|
|
47
|
+
from pathlib import Path
|
|
48
|
+
|
|
49
|
+
from andon_client import AndonView
|
|
50
|
+
|
|
51
|
+
# The .env from the configurator, as it is: ANDON_VIEW, ANDON_WRITE_SECRET,
|
|
52
|
+
# ANDON_CONTENT_KEY, ANDON_KEY_VERSION. Variables set in the environment win.
|
|
53
|
+
view = AndonView.from_env(env_file='.env', template=Path('view.json'))
|
|
54
|
+
|
|
55
|
+
view.set('t_oee', 79.3) # one tile
|
|
56
|
+
view.set({'t_oee': 79.3, 't_shift': 580}) # several at once
|
|
57
|
+
view.set('t_takt', 31.5, at=datetime.now(timezone.utc)) # appended to a series
|
|
58
|
+
|
|
59
|
+
view.close() # sends what is still pending
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The package collects, checks, encrypts and uploads, in a thread of its own. A
|
|
63
|
+
script that sets its values and ends still sends them: Python waits for that
|
|
64
|
+
thread until they are out, at the end of the upload interval if one was sent just
|
|
65
|
+
before. When the relay cannot be reached then, that try is the last one: the script
|
|
66
|
+
ends, and `on_error` (or the log) says what was not sent. `close()` sends at once;
|
|
67
|
+
a long-running process calls it when it stops, or uses the view in a `with` block.
|
|
68
|
+
|
|
69
|
+
The same package exists for Node.js,
|
|
70
|
+
[`@smartrsolutions/andon-client`](https://www.npmjs.com/package/@smartrsolutions/andon-client),
|
|
71
|
+
and for Node-RED, [`@smartrsolutions/node-red-andon`](https://www.npmjs.com/package/@smartrsolutions/node-red-andon),
|
|
72
|
+
with the same rules.
|
|
73
|
+
|
|
74
|
+
## Values
|
|
75
|
+
|
|
76
|
+
`set(tile_id, value, at=None, series=None, lane=None)` gives one tile a value.
|
|
77
|
+
What the value does depends on the tile:
|
|
78
|
+
|
|
79
|
+
| Tile | Value | Effect |
|
|
80
|
+
|---|---|---|
|
|
81
|
+
| label | number or text | replaces the value |
|
|
82
|
+
| gauge, progress, donut | number | replaces the value |
|
|
83
|
+
| line, bar over time | number | appended to the series, now or at `at`; `series` picks the series |
|
|
84
|
+
| line, bar over time | list of `[timestamp, value]` pairs | replaces the series; `series` picks which |
|
|
85
|
+
| line, bar over time | `{'series': [{'name', 'samples'}]}` | replaces every series |
|
|
86
|
+
| line, bar over time | `{'series': [{'name', 'start', 'stepSec', 'points'}]}` | replaces every series: a point every `stepSec` from `start` |
|
|
87
|
+
| bar with categories, pie, donut | list of numbers, one per category | replaces the values; `series` picks the series |
|
|
88
|
+
| bar with categories, pie, donut | `{'categories', 'series': [{'name', 'values'}]}` | replaces categories and values together |
|
|
89
|
+
| bar with categories, one series | `{'series': [{'name', 'values', 'statuses'}]}` | a verdict per bar: `ok`, `warning`, `critical` or `None` |
|
|
90
|
+
| table | list of rows, a row a list of cells | replaces the rows |
|
|
91
|
+
| table | list of `{'cells', 'status'}` | replaces the rows, with a status per row |
|
|
92
|
+
| timeline | state ID | starts a new segment, now or at `at`; `lane` picks the lane |
|
|
93
|
+
| timeline | `{'from', 'to'}` | moves the time axis; what lies before `from` is trimmed |
|
|
94
|
+
| any tile | dict, e.g. `{'value': 79.3, 'status': 'warning'}` | sets the named fields; `None` removes one |
|
|
95
|
+
|
|
96
|
+
`at` is a `datetime` (a naive one is local time, as Python takes it), seconds since
|
|
97
|
+
the epoch as `time.time()` gives them, or ISO 8601 text. `series` and `lane` are
|
|
98
|
+
names. Numbers may arrive as text (`"79.3"`), as MQTT and most PLCs deliver them.
|
|
99
|
+
Timestamps inside values are ISO 8601 in UTC with a trailing `Z`, such as
|
|
100
|
+
`2026-09-22T18:30:05Z`.
|
|
101
|
+
|
|
102
|
+
`set({tile_id: value, ...}, at=..., series=..., lane=...)` sets several tiles at
|
|
103
|
+
once; each value takes the same forms, and the call is taken or refused as a whole.
|
|
104
|
+
Values set together go out in the same upload.
|
|
105
|
+
|
|
106
|
+
`replace(document)` swaps in a whole view document, template included: for a source
|
|
107
|
+
that builds its tiles itself rather than filling those of a template.
|
|
108
|
+
`generatedAt` and `staleAfterSec` are set on upload.
|
|
109
|
+
|
|
110
|
+
A value that does not fit its tile raises `InputError`, one that would leave the
|
|
111
|
+
view invalid raises `ValidationError`, both a `ValueError`; the message says what
|
|
112
|
+
the tile takes, and the view stays as it was. The first real value of a series or
|
|
113
|
+
timeline lane replaces the points the template came with: those are the
|
|
114
|
+
configurator's start values.
|
|
115
|
+
|
|
116
|
+
## When it uploads
|
|
117
|
+
|
|
118
|
+
- A change goes out at most once per **interval** (`interval_sec`, 30 by default,
|
|
119
|
+
10 at least). The first change after a quiet spell goes out at once.
|
|
120
|
+
- While values keep arriving, even unchanged ones, a heartbeat goes out after half
|
|
121
|
+
the **stale** time (`stale_after_sec`, three intervals by default).
|
|
122
|
+
- When no value arrives, nothing is sent: the app then shows the view as stale,
|
|
123
|
+
which is what it should show when your source has stopped.
|
|
124
|
+
- Devices are woken only when a tile's **status** changes (`notify='auto'`;
|
|
125
|
+
`'always'` or `'never'`). Every wake-up costs battery on every paired device.
|
|
126
|
+
|
|
127
|
+
`flush()` sends a pending change now and returns the relay's answer; `close()`
|
|
128
|
+
sends what is pending and stops the thread (`close(flush=False)` drops it).
|
|
129
|
+
|
|
130
|
+
## Status
|
|
131
|
+
|
|
132
|
+
The status of a tile (`ok`, `warning`, `critical`) is your judgement: the app does
|
|
133
|
+
not compute it from the value. Send it with the value, or name the tile in
|
|
134
|
+
`derive_status` to have it derived from its bands.
|
|
135
|
+
|
|
136
|
+
## Options
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
AndonView(
|
|
140
|
+
view, # vw_..., from the .env
|
|
141
|
+
write_secret, # ws_..., from the .env
|
|
142
|
+
content_key, # base64, from the .env
|
|
143
|
+
key_version=1, # from the .env
|
|
144
|
+
template=None, # the template: a dict, its JSON text, or a path to view.json
|
|
145
|
+
interval_sec=30,
|
|
146
|
+
stale_after_sec=None, # three intervals
|
|
147
|
+
notify='auto',
|
|
148
|
+
window=None, # {'points': 60, 'age_sec': 0}: how much of a series is kept
|
|
149
|
+
derive_status=(), # tile IDs whose status follows from their bands
|
|
150
|
+
on_sent=None, # f(report), after every stored upload
|
|
151
|
+
on_error=None, # f(failure): message, status, retry_in_sec; logged without it
|
|
152
|
+
)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
`AndonView.from_env(prefix=None, env=None, env_file=None, **options)` reads the
|
|
156
|
+
keys from the environment, and from a `.env` file if you name one. A prefix such as
|
|
157
|
+
`ANDON_CLOUD_` replaces the leading `ANDON_` of every name, so several views can
|
|
158
|
+
share one environment.
|
|
159
|
+
|
|
160
|
+
After an upload failed, `on_error` says when the next try is (`retry_in_sec`). A
|
|
161
|
+
rejected write secret or an unknown view stops the uploads until the next value:
|
|
162
|
+
trying again changes nothing until the `.env` is current. Without `on_error`, the
|
|
163
|
+
message goes to the `andon_client` logger as a warning.
|
|
164
|
+
|
|
165
|
+
`on_sent` and `on_error` run in the thread that uploads: the package's own, or
|
|
166
|
+
yours during `flush()` and `close()`. Keep them short.
|
|
167
|
+
|
|
168
|
+
## Security
|
|
169
|
+
|
|
170
|
+
- The content key never leaves your process. The write secret is sent only in the
|
|
171
|
+
`Authorization` header of the upload, and neither appears in a report or an
|
|
172
|
+
error message.
|
|
173
|
+
- The `.env` from the configurator also holds the invite secret. This package does
|
|
174
|
+
not read it: a data source does not pair devices.
|
|
175
|
+
|
|
176
|
+
## Requirements
|
|
177
|
+
|
|
178
|
+
Python 3.9 or newer. The package depends on
|
|
179
|
+
[`cryptography`](https://pypi.org/project/cryptography/) for the encryption and
|
|
180
|
+
[`jsonschema`](https://pypi.org/project/jsonschema/) for the checks; HTTP goes
|
|
181
|
+
through the standard library, and honours the usual proxy variables.
|
|
182
|
+
|
|
183
|
+
## Contributing
|
|
184
|
+
|
|
185
|
+
The package is developed in a private repository next to the Andon relay and its
|
|
186
|
+
specification; its GitHub repository is a public mirror that receives every release
|
|
187
|
+
as one commit. Issues are welcome there, or by mail to <support@andon.app>. Pull
|
|
188
|
+
requests cannot be merged in the mirror, because the next release would overwrite
|
|
189
|
+
them: a change we take arrives with the next release. Security problems: please
|
|
190
|
+
write to <support@andon.app> instead of opening a public issue.
|
|
191
|
+
|
|
192
|
+
## License
|
|
193
|
+
|
|
194
|
+
Copyright 2026 SMARTR.solutions. MIT License; the text is in the file `LICENSE` of
|
|
195
|
+
this package.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Andon client for Python
|
|
2
|
+
|
|
3
|
+
Send values from Python to [Andon](https://andon.app), the app for iPhone and iPad
|
|
4
|
+
that shows your data as tiles and widgets. Everything is end-to-end encrypted: the
|
|
5
|
+
content key stays in your process, and only the devices you paired can read what
|
|
6
|
+
you send.
|
|
7
|
+
|
|
8
|
+
**Get the app:** [Andon on the App Store](https://apps.apple.com/app/id6819064723).
|
|
9
|
+
|
|
10
|
+
## Getting started
|
|
11
|
+
|
|
12
|
+
1. **Design** your tiles in the [Andon configurator](https://andon.app/en/configurator)
|
|
13
|
+
and create the view there. Download the `.env` and the document, `view.json`:
|
|
14
|
+
it is the template your values fill.
|
|
15
|
+
2. **Pair** your iPhone or iPad: scan the configurator's QR code with the
|
|
16
|
+
[Andon app](https://apps.apple.com/app/id6819064723).
|
|
17
|
+
3. **Install** this package: `pip install andon-client`.
|
|
18
|
+
4. **Send values**, by tile ID:
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from datetime import datetime, timezone
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from andon_client import AndonView
|
|
25
|
+
|
|
26
|
+
# The .env from the configurator, as it is: ANDON_VIEW, ANDON_WRITE_SECRET,
|
|
27
|
+
# ANDON_CONTENT_KEY, ANDON_KEY_VERSION. Variables set in the environment win.
|
|
28
|
+
view = AndonView.from_env(env_file='.env', template=Path('view.json'))
|
|
29
|
+
|
|
30
|
+
view.set('t_oee', 79.3) # one tile
|
|
31
|
+
view.set({'t_oee': 79.3, 't_shift': 580}) # several at once
|
|
32
|
+
view.set('t_takt', 31.5, at=datetime.now(timezone.utc)) # appended to a series
|
|
33
|
+
|
|
34
|
+
view.close() # sends what is still pending
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The package collects, checks, encrypts and uploads, in a thread of its own. A
|
|
38
|
+
script that sets its values and ends still sends them: Python waits for that
|
|
39
|
+
thread until they are out, at the end of the upload interval if one was sent just
|
|
40
|
+
before. When the relay cannot be reached then, that try is the last one: the script
|
|
41
|
+
ends, and `on_error` (or the log) says what was not sent. `close()` sends at once;
|
|
42
|
+
a long-running process calls it when it stops, or uses the view in a `with` block.
|
|
43
|
+
|
|
44
|
+
The same package exists for Node.js,
|
|
45
|
+
[`@smartrsolutions/andon-client`](https://www.npmjs.com/package/@smartrsolutions/andon-client),
|
|
46
|
+
and for Node-RED, [`@smartrsolutions/node-red-andon`](https://www.npmjs.com/package/@smartrsolutions/node-red-andon),
|
|
47
|
+
with the same rules.
|
|
48
|
+
|
|
49
|
+
## Values
|
|
50
|
+
|
|
51
|
+
`set(tile_id, value, at=None, series=None, lane=None)` gives one tile a value.
|
|
52
|
+
What the value does depends on the tile:
|
|
53
|
+
|
|
54
|
+
| Tile | Value | Effect |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| label | number or text | replaces the value |
|
|
57
|
+
| gauge, progress, donut | number | replaces the value |
|
|
58
|
+
| line, bar over time | number | appended to the series, now or at `at`; `series` picks the series |
|
|
59
|
+
| line, bar over time | list of `[timestamp, value]` pairs | replaces the series; `series` picks which |
|
|
60
|
+
| line, bar over time | `{'series': [{'name', 'samples'}]}` | replaces every series |
|
|
61
|
+
| line, bar over time | `{'series': [{'name', 'start', 'stepSec', 'points'}]}` | replaces every series: a point every `stepSec` from `start` |
|
|
62
|
+
| bar with categories, pie, donut | list of numbers, one per category | replaces the values; `series` picks the series |
|
|
63
|
+
| bar with categories, pie, donut | `{'categories', 'series': [{'name', 'values'}]}` | replaces categories and values together |
|
|
64
|
+
| bar with categories, one series | `{'series': [{'name', 'values', 'statuses'}]}` | a verdict per bar: `ok`, `warning`, `critical` or `None` |
|
|
65
|
+
| table | list of rows, a row a list of cells | replaces the rows |
|
|
66
|
+
| table | list of `{'cells', 'status'}` | replaces the rows, with a status per row |
|
|
67
|
+
| timeline | state ID | starts a new segment, now or at `at`; `lane` picks the lane |
|
|
68
|
+
| timeline | `{'from', 'to'}` | moves the time axis; what lies before `from` is trimmed |
|
|
69
|
+
| any tile | dict, e.g. `{'value': 79.3, 'status': 'warning'}` | sets the named fields; `None` removes one |
|
|
70
|
+
|
|
71
|
+
`at` is a `datetime` (a naive one is local time, as Python takes it), seconds since
|
|
72
|
+
the epoch as `time.time()` gives them, or ISO 8601 text. `series` and `lane` are
|
|
73
|
+
names. Numbers may arrive as text (`"79.3"`), as MQTT and most PLCs deliver them.
|
|
74
|
+
Timestamps inside values are ISO 8601 in UTC with a trailing `Z`, such as
|
|
75
|
+
`2026-09-22T18:30:05Z`.
|
|
76
|
+
|
|
77
|
+
`set({tile_id: value, ...}, at=..., series=..., lane=...)` sets several tiles at
|
|
78
|
+
once; each value takes the same forms, and the call is taken or refused as a whole.
|
|
79
|
+
Values set together go out in the same upload.
|
|
80
|
+
|
|
81
|
+
`replace(document)` swaps in a whole view document, template included: for a source
|
|
82
|
+
that builds its tiles itself rather than filling those of a template.
|
|
83
|
+
`generatedAt` and `staleAfterSec` are set on upload.
|
|
84
|
+
|
|
85
|
+
A value that does not fit its tile raises `InputError`, one that would leave the
|
|
86
|
+
view invalid raises `ValidationError`, both a `ValueError`; the message says what
|
|
87
|
+
the tile takes, and the view stays as it was. The first real value of a series or
|
|
88
|
+
timeline lane replaces the points the template came with: those are the
|
|
89
|
+
configurator's start values.
|
|
90
|
+
|
|
91
|
+
## When it uploads
|
|
92
|
+
|
|
93
|
+
- A change goes out at most once per **interval** (`interval_sec`, 30 by default,
|
|
94
|
+
10 at least). The first change after a quiet spell goes out at once.
|
|
95
|
+
- While values keep arriving, even unchanged ones, a heartbeat goes out after half
|
|
96
|
+
the **stale** time (`stale_after_sec`, three intervals by default).
|
|
97
|
+
- When no value arrives, nothing is sent: the app then shows the view as stale,
|
|
98
|
+
which is what it should show when your source has stopped.
|
|
99
|
+
- Devices are woken only when a tile's **status** changes (`notify='auto'`;
|
|
100
|
+
`'always'` or `'never'`). Every wake-up costs battery on every paired device.
|
|
101
|
+
|
|
102
|
+
`flush()` sends a pending change now and returns the relay's answer; `close()`
|
|
103
|
+
sends what is pending and stops the thread (`close(flush=False)` drops it).
|
|
104
|
+
|
|
105
|
+
## Status
|
|
106
|
+
|
|
107
|
+
The status of a tile (`ok`, `warning`, `critical`) is your judgement: the app does
|
|
108
|
+
not compute it from the value. Send it with the value, or name the tile in
|
|
109
|
+
`derive_status` to have it derived from its bands.
|
|
110
|
+
|
|
111
|
+
## Options
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
AndonView(
|
|
115
|
+
view, # vw_..., from the .env
|
|
116
|
+
write_secret, # ws_..., from the .env
|
|
117
|
+
content_key, # base64, from the .env
|
|
118
|
+
key_version=1, # from the .env
|
|
119
|
+
template=None, # the template: a dict, its JSON text, or a path to view.json
|
|
120
|
+
interval_sec=30,
|
|
121
|
+
stale_after_sec=None, # three intervals
|
|
122
|
+
notify='auto',
|
|
123
|
+
window=None, # {'points': 60, 'age_sec': 0}: how much of a series is kept
|
|
124
|
+
derive_status=(), # tile IDs whose status follows from their bands
|
|
125
|
+
on_sent=None, # f(report), after every stored upload
|
|
126
|
+
on_error=None, # f(failure): message, status, retry_in_sec; logged without it
|
|
127
|
+
)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`AndonView.from_env(prefix=None, env=None, env_file=None, **options)` reads the
|
|
131
|
+
keys from the environment, and from a `.env` file if you name one. A prefix such as
|
|
132
|
+
`ANDON_CLOUD_` replaces the leading `ANDON_` of every name, so several views can
|
|
133
|
+
share one environment.
|
|
134
|
+
|
|
135
|
+
After an upload failed, `on_error` says when the next try is (`retry_in_sec`). A
|
|
136
|
+
rejected write secret or an unknown view stops the uploads until the next value:
|
|
137
|
+
trying again changes nothing until the `.env` is current. Without `on_error`, the
|
|
138
|
+
message goes to the `andon_client` logger as a warning.
|
|
139
|
+
|
|
140
|
+
`on_sent` and `on_error` run in the thread that uploads: the package's own, or
|
|
141
|
+
yours during `flush()` and `close()`. Keep them short.
|
|
142
|
+
|
|
143
|
+
## Security
|
|
144
|
+
|
|
145
|
+
- The content key never leaves your process. The write secret is sent only in the
|
|
146
|
+
`Authorization` header of the upload, and neither appears in a report or an
|
|
147
|
+
error message.
|
|
148
|
+
- The `.env` from the configurator also holds the invite secret. This package does
|
|
149
|
+
not read it: a data source does not pair devices.
|
|
150
|
+
|
|
151
|
+
## Requirements
|
|
152
|
+
|
|
153
|
+
Python 3.9 or newer. The package depends on
|
|
154
|
+
[`cryptography`](https://pypi.org/project/cryptography/) for the encryption and
|
|
155
|
+
[`jsonschema`](https://pypi.org/project/jsonschema/) for the checks; HTTP goes
|
|
156
|
+
through the standard library, and honours the usual proxy variables.
|
|
157
|
+
|
|
158
|
+
## Contributing
|
|
159
|
+
|
|
160
|
+
The package is developed in a private repository next to the Andon relay and its
|
|
161
|
+
specification; its GitHub repository is a public mirror that receives every release
|
|
162
|
+
as one commit. Issues are welcome there, or by mail to <support@andon.app>. Pull
|
|
163
|
+
requests cannot be merged in the mirror, because the next release would overwrite
|
|
164
|
+
them: a change we take arrives with the next release. Security problems: please
|
|
165
|
+
write to <support@andon.app> instead of opening a public issue.
|
|
166
|
+
|
|
167
|
+
## License
|
|
168
|
+
|
|
169
|
+
Copyright 2026 SMARTR.solutions. MIT License; the text is in the file `LICENSE` of
|
|
170
|
+
this package.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "andon-client"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "Send values from Python to the Andon app for iPhone and iPad: end-to-end encrypted tiles and widgets."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
license-files = ["LICENSE"]
|
|
8
|
+
requires-python = ">=3.9"
|
|
9
|
+
authors = [{ name = "SMARTR.solutions", email = "support@andon.app" }]
|
|
10
|
+
keywords = ["andon", "iphone", "ipad", "dashboard", "widget", "iot"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 5 - Production/Stable",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"Intended Audience :: Manufacturing",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
18
|
+
"Topic :: System :: Monitoring",
|
|
19
|
+
"Typing :: Typed",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"cryptography>=3.4",
|
|
23
|
+
"jsonschema>=4.18",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://andon.app/en/developers"
|
|
28
|
+
Source = "https://github.com/smartrsolutions/andon-client-python"
|
|
29
|
+
Changelog = "https://github.com/smartrsolutions/andon-client-python/blob/main/CHANGELOG.md"
|
|
30
|
+
Issues = "https://github.com/smartrsolutions/andon-client-python/issues"
|
|
31
|
+
|
|
32
|
+
[build-system]
|
|
33
|
+
requires = ["hatchling>=1.27"]
|
|
34
|
+
build-backend = "hatchling.build"
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.targets.wheel]
|
|
37
|
+
packages = ["src/andon_client"]
|
|
38
|
+
|
|
39
|
+
# The parity vectors stay in the repository: 2 MB that only its tests read.
|
|
40
|
+
[tool.hatch.build.targets.sdist]
|
|
41
|
+
include = ["src/", "tests/", "README.md", "CHANGELOG.md", "LICENSE"]
|
|
42
|
+
exclude = ["tests/data/parity.json", "tests/test_parity.py"]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
andon-client: send values from Python to the Andon app for iPhone and iPad.
|
|
3
|
+
|
|
4
|
+
What is exported here is the public API, covered by semantic versioning;
|
|
5
|
+
everything in the modules whose names start with an underscore is internal.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from ._relay import PutResult
|
|
9
|
+
from ._runtime import ReceiveResult, UploadReport, ValidationError
|
|
10
|
+
from ._tiles import InputError
|
|
11
|
+
from ._validate import Issue, Validation, validate
|
|
12
|
+
from ._view import AndonView, UploadFailure
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
'AndonView',
|
|
16
|
+
'InputError',
|
|
17
|
+
'Issue',
|
|
18
|
+
'PutResult',
|
|
19
|
+
'ReceiveResult',
|
|
20
|
+
'UploadFailure',
|
|
21
|
+
'UploadReport',
|
|
22
|
+
'Validation',
|
|
23
|
+
'ValidationError',
|
|
24
|
+
'validate',
|
|
25
|
+
]
|