readerboard 0.4.0__tar.gz → 0.6.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.
- {readerboard-0.4.0/readerboard.egg-info → readerboard-0.6.0}/PKG-INFO +140 -16
- {readerboard-0.4.0 → readerboard-0.6.0}/README.md +139 -15
- {readerboard-0.4.0 → readerboard-0.6.0}/pyproject.toml +1 -1
- {readerboard-0.4.0 → readerboard-0.6.0}/readerboard/__init__.py +1 -1
- {readerboard-0.4.0 → readerboard-0.6.0}/readerboard/api/app.py +34 -21
- {readerboard-0.4.0 → readerboard-0.6.0}/readerboard/api/deps.py +4 -4
- {readerboard-0.4.0 → readerboard-0.6.0}/readerboard/api/errors.py +15 -1
- {readerboard-0.4.0 → readerboard-0.6.0}/readerboard/api/models.py +161 -7
- {readerboard-0.4.0 → readerboard-0.6.0}/readerboard/api/routes.py +199 -31
- {readerboard-0.4.0 → readerboard-0.6.0}/readerboard/config.py +46 -10
- {readerboard-0.4.0 → readerboard-0.6.0}/readerboard/protocol/constants.py +43 -9
- {readerboard-0.4.0 → readerboard-0.6.0}/readerboard/protocol/frames.py +77 -8
- {readerboard-0.4.0 → readerboard-0.6.0}/readerboard/protocol/markup.py +143 -3
- {readerboard-0.4.0 → readerboard-0.6.0}/readerboard/protocol/replies.py +5 -4
- {readerboard-0.4.0 → readerboard-0.6.0}/readerboard/protocol/tokens.py +6 -5
- {readerboard-0.4.0 → readerboard-0.6.0}/readerboard/services/alerts.py +56 -48
- readerboard-0.6.0/readerboard/services/registry.py +952 -0
- {readerboard-0.4.0 → readerboard-0.6.0}/readerboard/sign/controller.py +45 -20
- readerboard-0.6.0/readerboard/sign/layout.py +157 -0
- {readerboard-0.4.0 → readerboard-0.6.0}/readerboard/sign/state.py +72 -10
- {readerboard-0.4.0 → readerboard-0.6.0}/readerboard/transport/base.py +1 -1
- {readerboard-0.4.0 → readerboard-0.6.0}/readerboard/transport/serial_link.py +21 -4
- {readerboard-0.4.0 → readerboard-0.6.0/readerboard.egg-info}/PKG-INFO +140 -16
- {readerboard-0.4.0 → readerboard-0.6.0}/readerboard.egg-info/SOURCES.txt +3 -1
- {readerboard-0.4.0 → readerboard-0.6.0}/tests/test_api.py +317 -48
- {readerboard-0.4.0 → readerboard-0.6.0}/tests/test_component_names.py +7 -0
- readerboard-0.6.0/tests/test_config.py +47 -0
- {readerboard-0.4.0 → readerboard-0.6.0}/tests/test_constant_values.py +42 -11
- {readerboard-0.4.0 → readerboard-0.6.0}/tests/test_controller.py +110 -13
- {readerboard-0.4.0 → readerboard-0.6.0}/tests/test_frames.py +63 -0
- {readerboard-0.4.0 → readerboard-0.6.0}/tests/test_markup.py +119 -1
- readerboard-0.6.0/tests/test_registry.py +863 -0
- {readerboard-0.4.0 → readerboard-0.6.0}/tests/test_state.py +97 -20
- {readerboard-0.4.0 → readerboard-0.6.0}/tests/test_transport.py +70 -1
- readerboard-0.6.0/tests/test_variables.py +613 -0
- readerboard-0.4.0/readerboard/services/registry.py +0 -432
- readerboard-0.4.0/readerboard/sign/layout.py +0 -116
- readerboard-0.4.0/tests/test_registry.py +0 -479
- {readerboard-0.4.0 → readerboard-0.6.0}/LICENSE +0 -0
- {readerboard-0.4.0 → readerboard-0.6.0}/readerboard/__main__.py +0 -0
- {readerboard-0.4.0 → readerboard-0.6.0}/readerboard/api/__init__.py +0 -0
- {readerboard-0.4.0 → readerboard-0.6.0}/readerboard/logging_setup.py +0 -0
- {readerboard-0.4.0 → readerboard-0.6.0}/readerboard/names.py +0 -0
- {readerboard-0.4.0 → readerboard-0.6.0}/readerboard/protocol/__init__.py +0 -0
- {readerboard-0.4.0 → readerboard-0.6.0}/readerboard/py.typed +0 -0
- {readerboard-0.4.0 → readerboard-0.6.0}/readerboard/services/__init__.py +0 -0
- {readerboard-0.4.0 → readerboard-0.6.0}/readerboard/services/clock.py +0 -0
- {readerboard-0.4.0 → readerboard-0.6.0}/readerboard/services/commands.py +0 -0
- {readerboard-0.4.0 → readerboard-0.6.0}/readerboard/sign/__init__.py +0 -0
- {readerboard-0.4.0 → readerboard-0.6.0}/readerboard/transport/__init__.py +0 -0
- {readerboard-0.4.0 → readerboard-0.6.0}/readerboard/transport/fake.py +0 -0
- {readerboard-0.4.0 → readerboard-0.6.0}/readerboard.egg-info/dependency_links.txt +0 -0
- {readerboard-0.4.0 → readerboard-0.6.0}/readerboard.egg-info/entry_points.txt +0 -0
- {readerboard-0.4.0 → readerboard-0.6.0}/readerboard.egg-info/requires.txt +0 -0
- {readerboard-0.4.0 → readerboard-0.6.0}/readerboard.egg-info/top_level.txt +0 -0
- {readerboard-0.4.0 → readerboard-0.6.0}/setup.cfg +0 -0
- {readerboard-0.4.0 → readerboard-0.6.0}/tests/test_alerts.py +0 -0
- {readerboard-0.4.0 → readerboard-0.6.0}/tests/test_clock.py +0 -0
- {readerboard-0.4.0 → readerboard-0.6.0}/tests/test_launch_configurations.py +0 -0
- {readerboard-0.4.0 → readerboard-0.6.0}/tests/test_logging_setup.py +0 -0
- {readerboard-0.4.0 → readerboard-0.6.0}/tests/test_open_docs.py +0 -0
- {readerboard-0.4.0 → readerboard-0.6.0}/tests/test_replies.py +0 -0
- {readerboard-0.4.0 → readerboard-0.6.0}/tests/test_run_against_a_sign.py +0 -0
- {readerboard-0.4.0 → readerboard-0.6.0}/tests/test_run_with_simulator.py +0 -0
- {readerboard-0.4.0 → readerboard-0.6.0}/tests/test_tool_icons.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: readerboard
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: An HTTP service for BetaBrite and Alpha protocol LED signs: several sources share one sign, with alerts, expiring messages and clock sync
|
|
5
5
|
Author: mjaksn
|
|
6
6
|
License-Expression: MIT
|
|
@@ -55,8 +55,12 @@ An HTTP service that drives a BetaBrite Classic sign, either through a serial ca
|
|
|
55
55
|
through an Ethernet to RS-232 adapter.
|
|
56
56
|
|
|
57
57
|
Several sources can share the sign at once. Each registers a named **slot**, and the sign
|
|
58
|
-
rotates through the
|
|
59
|
-
|
|
58
|
+
rotates through the slots that are showing by itself. A slot can be hidden without being
|
|
59
|
+
given up, so a message can be taken off the display and put back without being sent again,
|
|
60
|
+
unless it was the last one showing. A **variable** is a value that messages call by name,
|
|
61
|
+
such as a temperature, and changing it does not blank the sign or restart the message
|
|
62
|
+
showing it. An **alert** takes the whole display over until it is released, after which
|
|
63
|
+
the rotation resumes.
|
|
60
64
|
|
|
61
65
|
## What it does
|
|
62
66
|
|
|
@@ -64,6 +68,10 @@ until it is released, after which the rotation resumes.
|
|
|
64
68
|
automation owns `doorbell`, without either knowing about the other.
|
|
65
69
|
- **The sign does the rotating.** Each message lives in its own sign file and the sign
|
|
66
70
|
cycles them on its own, so rotation costs no serial traffic at all.
|
|
71
|
+
- **Live values without a blink.** A message such as `Outside <var:temp><degree>F` calls
|
|
72
|
+
the variable `temp`, and a new value is one small write that the sign shows the next
|
|
73
|
+
time it draws the message, with no blank and no restart. One variable can appear in
|
|
74
|
+
any number of messages, and a value that stops arriving can be made to go stale.
|
|
67
75
|
- **Alerts.** Take the display over, optionally with a deadline, then hand it back.
|
|
68
76
|
- **It keeps the sign's clock right**, at startup, hourly, and whenever the link comes
|
|
69
77
|
back. That last trigger is the one that matters: a sign returning from a power cut
|
|
@@ -76,8 +84,10 @@ until it is released, after which the rotation resumes.
|
|
|
76
84
|
flicker.
|
|
77
85
|
- **It survives restarts and outages.** The registered messages are persisted and
|
|
78
86
|
pushed to the sign again whenever the link returns, so a restart or a power cut leaves
|
|
79
|
-
the rotation intact. A write that arrives while the sign is
|
|
80
|
-
a 503 rather than silently held, so the caller learns it did
|
|
87
|
+
the rotation intact. A message or a variable write that arrives while the sign is
|
|
88
|
+
unreachable is refused with a 503 rather than silently held, so the caller learns it did
|
|
89
|
+
not land. Deleting or hiding a message, or deleting a variable, is accepted and carried
|
|
90
|
+
out when the link returns.
|
|
81
91
|
- **Errors are errors.** A dead serial link is a 503 and a message the sign cannot
|
|
82
92
|
render is a 400, each with the reason in the body. Nothing here reports a failure
|
|
83
93
|
under a 200.
|
|
@@ -257,7 +267,7 @@ service's other reads and `GET /health` do not. In the Swagger UI at `/docs`, th
|
|
|
257
267
|
Register a message:
|
|
258
268
|
|
|
259
269
|
```
|
|
260
|
-
curl -X PUT http://localhost:5001/
|
|
270
|
+
curl -X PUT http://localhost:5001/slots/temperature \
|
|
261
271
|
-H 'X-API-Key: YOUR-KEY' -H 'Content-Type: application/json' \
|
|
262
272
|
-d '{"message": "<green>18.4<degree> <red><time>", "display_mode": "HOLD"}'
|
|
263
273
|
```
|
|
@@ -265,11 +275,70 @@ curl -X PUT http://localhost:5001/messages/temperature \
|
|
|
265
275
|
Register a second one and the sign rotates between them:
|
|
266
276
|
|
|
267
277
|
```
|
|
268
|
-
curl -X PUT http://localhost:5001/
|
|
278
|
+
curl -X PUT http://localhost:5001/slots/doorbell \
|
|
269
279
|
-H 'X-API-Key: YOUR-KEY' -H 'Content-Type: application/json' \
|
|
270
280
|
-d '{"message": "<amber>Someone at the door", "ttl_seconds": 300}'
|
|
271
281
|
```
|
|
272
282
|
|
|
283
|
+
Take a message off the display without giving up its slot, and put it back later:
|
|
284
|
+
|
|
285
|
+
```
|
|
286
|
+
curl -X PUT http://localhost:5001/slots/doorbell/active \
|
|
287
|
+
-H 'X-API-Key: YOUR-KEY' -H 'Content-Type: application/json' \
|
|
288
|
+
-d '{"active": false}'
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
A hidden message keeps its slot, its place in the order and its text, so showing it again
|
|
292
|
+
takes `{"active": true}` and no copy of what it said. Hiding or showing one is a single run
|
|
293
|
+
sequence write. That does disturb the display briefly, but far less than rewriting a
|
|
294
|
+
message does: enough less that it is easy to miss unless you are watching for it on a
|
|
295
|
+
static screen. The exception is the last message showing: its file is emptied as it goes,
|
|
296
|
+
because a sign whose run sequence names nothing freezes on what it was drawing, so
|
|
297
|
+
putting that one back costs the text as well as the sequence.
|
|
298
|
+
|
|
299
|
+
`active` is a field on the message endpoint too, where leaving it out is the point: a
|
|
300
|
+
source re-sending the same content every few minutes says nothing about it and so cannot
|
|
301
|
+
switch back on something that was deliberately hidden. Sending it moves the message.
|
|
302
|
+
|
|
303
|
+
A `ttl_seconds` can hide a message instead of deleting it, which suits anything that comes
|
|
304
|
+
back later, such as a bin day or a school notice:
|
|
305
|
+
|
|
306
|
+
```
|
|
307
|
+
curl -X PUT http://localhost:5001/slots/bins \
|
|
308
|
+
-H 'X-API-Key: YOUR-KEY' -H 'Content-Type: application/json' \
|
|
309
|
+
-d '{"message": "<green>BINS OUT TONIGHT", "ttl_seconds": 43200,
|
|
310
|
+
"delete_on_expiry": false}'
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
Put those together and a recurring notification is one call per event, sent in the same
|
|
314
|
+
shape every time. This shows the alarm's state for a minute whenever it changes, then takes
|
|
315
|
+
it off the rotation until the next one:
|
|
316
|
+
|
|
317
|
+
```
|
|
318
|
+
curl -X PUT http://localhost:5001/slots/alarm \
|
|
319
|
+
-H 'X-API-Key: YOUR-KEY' -H 'Content-Type: application/json' \
|
|
320
|
+
-d '{"message": "<red>ALARM NOW <var:arm_state>", "ttl_seconds": 60,
|
|
321
|
+
"delete_on_expiry": false, "active": true}'
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
The deadline clears itself as it hides the message, so each event gets a fresh minute
|
|
325
|
+
rather than the message vanishing again on a deadline the last one left behind.
|
|
326
|
+
|
|
327
|
+
Show a live value. Create the variable first, then a message that calls it:
|
|
328
|
+
|
|
329
|
+
```
|
|
330
|
+
curl -X PUT http://localhost:5001/variables/temp \
|
|
331
|
+
-H 'X-API-Key: YOUR-KEY' -H 'Content-Type: application/json' \
|
|
332
|
+
-d '{"value": "72", "ttl_seconds": 1800, "stale_value": "--"}'
|
|
333
|
+
|
|
334
|
+
curl -X PUT http://localhost:5001/slots/weather \
|
|
335
|
+
-H 'X-API-Key: YOUR-KEY' -H 'Content-Type: application/json' \
|
|
336
|
+
-d '{"message": "Outside <var:temp><degree>F", "display_mode": "ROTATE"}'
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
From then on only the variable needs writing, and the message picks each new value up
|
|
340
|
+
on its next pass across the sign.
|
|
341
|
+
|
|
273
342
|
Take the sign over for thirty seconds:
|
|
274
343
|
|
|
275
344
|
```
|
|
@@ -296,8 +365,8 @@ Silence it with `{"command": "SPEAKER", "parameter": "OFF"}`, and turn it back o
|
|
|
296
365
|
lives on the sign and survives a restart, so it is also the first thing to check if
|
|
297
366
|
`SOUND` ever seems to do nothing.
|
|
298
367
|
|
|
299
|
-
The full API is at `/docs`. Every markup token, display mode and
|
|
300
|
-
command is listed by the `/enumerations` reads there, which answer at
|
|
368
|
+
The full API is at `/docs`. Every markup token, value token, display mode and
|
|
369
|
+
control command is listed by the `/enumerations` reads there, which answer at
|
|
301
370
|
request time rather than being frozen into the description.
|
|
302
371
|
|
|
303
372
|
### Writing messages
|
|
@@ -311,6 +380,59 @@ displays correctly. A character the sign cannot render is rejected with a 400, a
|
|
|
311
380
|
unknown token: a write is told what the sign would have made of it rather than being
|
|
312
381
|
shown something it did not ask for.
|
|
313
382
|
|
|
383
|
+
### Live values: variables
|
|
384
|
+
|
|
385
|
+
A variable lives in a small file of its own on the sign, and a message or an alert
|
|
386
|
+
calls it with `<var:name>`. Writing a new value rewrites that file and nothing else,
|
|
387
|
+
which the sign takes without blanking, so a message showing a temperature or a count can
|
|
388
|
+
change every minute and never restart. In a scrolling mode the new value appears on the
|
|
389
|
+
message's next pass. An alert carrying a live wind speed works the same way, and keeps
|
|
390
|
+
the sign while the number changes.
|
|
391
|
+
|
|
392
|
+
A few things are worth knowing, all of them measured on the sign:
|
|
393
|
+
|
|
394
|
+
- **Create the variable before a message calls it.** A message or an alert naming a
|
|
395
|
+
variable that does not exist is refused with a 400, and a variable that a message or
|
|
396
|
+
the alert still calls cannot be deleted: that is a 409 naming what calls it.
|
|
397
|
+
- **Formatting in a value carries on after it.** A value of `<red>DOWN` turns the rest
|
|
398
|
+
of the message red as well, and so does a character set or a speed. If the text after
|
|
399
|
+
the call matters, set it again in the message after the call.
|
|
400
|
+
- **A value takes the message markup, less two tokens.** `<week_day>` draws as a literal
|
|
401
|
+
`9` from inside a variable, and a variable cannot call another. `GET
|
|
402
|
+
/enumerations/value-tokens` lists what is allowed.
|
|
403
|
+
- **Keep a changing number from pushing the text around it.** With the sign's usual
|
|
404
|
+
proportional spacing, `11` and `88` are different widths and the text after them moves.
|
|
405
|
+
Put `<fixed_width>` in the message before the call and send values of the same length.
|
|
406
|
+
Fixed width also left-justifies the line.
|
|
407
|
+
- **A value that stops arriving can go stale.** Give a `ttl_seconds` and a `stale_value`
|
|
408
|
+
such as `--`, and once the time passes without a new value the sign shows the stale
|
|
409
|
+
value instead. The variable itself stays, since messages call it.
|
|
410
|
+
- **A value that does not fit is refused.** Each holds `variable_capacity` bytes after
|
|
411
|
+
rendering, 32 by default. The sign does not cut an overlong value short, it empties it,
|
|
412
|
+
so the service refuses one rather than send it.
|
|
413
|
+
|
|
414
|
+
A Home Assistant `rest_command` that keeps a sensor on the sign:
|
|
415
|
+
|
|
416
|
+
```yaml
|
|
417
|
+
rest_command:
|
|
418
|
+
sign_temperature:
|
|
419
|
+
url: http://readerboard.local:5001/variables/temp
|
|
420
|
+
method: put
|
|
421
|
+
headers:
|
|
422
|
+
X-API-Key: !secret readerboard_key
|
|
423
|
+
content_type: application/json
|
|
424
|
+
payload: '{"value": "{{ value }}", "ttl_seconds": 1800, "stale_value": "--"}'
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
Call it from an automation triggered by the sensor, with the reading as `value`:
|
|
428
|
+
|
|
429
|
+
```yaml
|
|
430
|
+
actions:
|
|
431
|
+
- action: rest_command.sign_temperature
|
|
432
|
+
data:
|
|
433
|
+
value: "{{ states('sensor.outside_temperature') | round(0) | int }}"
|
|
434
|
+
```
|
|
435
|
+
|
|
314
436
|
### Recovering a sign that has stopped responding
|
|
315
437
|
|
|
316
438
|
A sign mounted out of reach can wedge: a stray bit corrupts what its decoder is
|
|
@@ -339,10 +461,10 @@ from the service's own record, so the display still comes back to what it was.
|
|
|
339
461
|
curl -X POST http://localhost:5001/sign/reboot -H 'X-API-Key: YOUR-KEY'
|
|
340
462
|
```
|
|
341
463
|
|
|
342
|
-
Reach for it only when a soft reset was not enough. The sign is blank for
|
|
343
|
-
|
|
344
|
-
that without resetting anything. The client
|
|
345
|
-
confirmation for the same reason.
|
|
464
|
+
Reach for it only when a soft reset was not enough. The sign is blank for twelve seconds
|
|
465
|
+
or more while it resets, longer with a lot of messages to put back. Neither is a way to
|
|
466
|
+
clear messages: `DELETE /slots` does that without resetting anything. The client
|
|
467
|
+
fronts the reboot with a warning-coloured confirmation for the same reason.
|
|
346
468
|
|
|
347
469
|
## Configuration
|
|
348
470
|
|
|
@@ -361,9 +483,11 @@ telnet serial protocol, `/dev/ttyUSB0` or `COM3` for a cable plugged straight in
|
|
|
361
483
|
and the port, and pyserial's answer to one that has a slash names neither the setting
|
|
362
484
|
nor the value.
|
|
363
485
|
|
|
364
|
-
|
|
365
|
-
on it**: `slot_count` and `
|
|
366
|
-
the log, but they are not settings to fiddle
|
|
486
|
+
Four settings reallocate the sign's memory when changed, and **that erases every message
|
|
487
|
+
on it**: `slot_count`, `slot_capacity`, `variable_count` and `variable_capacity`. The
|
|
488
|
+
service will do it, and say so loudly in the log, but they are not settings to fiddle
|
|
489
|
+
with. With `variable_count` at 0, `variable_capacity` allocates nothing, so changing it
|
|
490
|
+
alone reallocates nothing either.
|
|
367
491
|
|
|
368
492
|
## Security
|
|
369
493
|
|
|
@@ -11,8 +11,12 @@ An HTTP service that drives a BetaBrite Classic sign, either through a serial ca
|
|
|
11
11
|
through an Ethernet to RS-232 adapter.
|
|
12
12
|
|
|
13
13
|
Several sources can share the sign at once. Each registers a named **slot**, and the sign
|
|
14
|
-
rotates through the
|
|
15
|
-
|
|
14
|
+
rotates through the slots that are showing by itself. A slot can be hidden without being
|
|
15
|
+
given up, so a message can be taken off the display and put back without being sent again,
|
|
16
|
+
unless it was the last one showing. A **variable** is a value that messages call by name,
|
|
17
|
+
such as a temperature, and changing it does not blank the sign or restart the message
|
|
18
|
+
showing it. An **alert** takes the whole display over until it is released, after which
|
|
19
|
+
the rotation resumes.
|
|
16
20
|
|
|
17
21
|
## What it does
|
|
18
22
|
|
|
@@ -20,6 +24,10 @@ until it is released, after which the rotation resumes.
|
|
|
20
24
|
automation owns `doorbell`, without either knowing about the other.
|
|
21
25
|
- **The sign does the rotating.** Each message lives in its own sign file and the sign
|
|
22
26
|
cycles them on its own, so rotation costs no serial traffic at all.
|
|
27
|
+
- **Live values without a blink.** A message such as `Outside <var:temp><degree>F` calls
|
|
28
|
+
the variable `temp`, and a new value is one small write that the sign shows the next
|
|
29
|
+
time it draws the message, with no blank and no restart. One variable can appear in
|
|
30
|
+
any number of messages, and a value that stops arriving can be made to go stale.
|
|
23
31
|
- **Alerts.** Take the display over, optionally with a deadline, then hand it back.
|
|
24
32
|
- **It keeps the sign's clock right**, at startup, hourly, and whenever the link comes
|
|
25
33
|
back. That last trigger is the one that matters: a sign returning from a power cut
|
|
@@ -32,8 +40,10 @@ until it is released, after which the rotation resumes.
|
|
|
32
40
|
flicker.
|
|
33
41
|
- **It survives restarts and outages.** The registered messages are persisted and
|
|
34
42
|
pushed to the sign again whenever the link returns, so a restart or a power cut leaves
|
|
35
|
-
the rotation intact. A write that arrives while the sign is
|
|
36
|
-
a 503 rather than silently held, so the caller learns it did
|
|
43
|
+
the rotation intact. A message or a variable write that arrives while the sign is
|
|
44
|
+
unreachable is refused with a 503 rather than silently held, so the caller learns it did
|
|
45
|
+
not land. Deleting or hiding a message, or deleting a variable, is accepted and carried
|
|
46
|
+
out when the link returns.
|
|
37
47
|
- **Errors are errors.** A dead serial link is a 503 and a message the sign cannot
|
|
38
48
|
render is a 400, each with the reason in the body. Nothing here reports a failure
|
|
39
49
|
under a 200.
|
|
@@ -213,7 +223,7 @@ service's other reads and `GET /health` do not. In the Swagger UI at `/docs`, th
|
|
|
213
223
|
Register a message:
|
|
214
224
|
|
|
215
225
|
```
|
|
216
|
-
curl -X PUT http://localhost:5001/
|
|
226
|
+
curl -X PUT http://localhost:5001/slots/temperature \
|
|
217
227
|
-H 'X-API-Key: YOUR-KEY' -H 'Content-Type: application/json' \
|
|
218
228
|
-d '{"message": "<green>18.4<degree> <red><time>", "display_mode": "HOLD"}'
|
|
219
229
|
```
|
|
@@ -221,11 +231,70 @@ curl -X PUT http://localhost:5001/messages/temperature \
|
|
|
221
231
|
Register a second one and the sign rotates between them:
|
|
222
232
|
|
|
223
233
|
```
|
|
224
|
-
curl -X PUT http://localhost:5001/
|
|
234
|
+
curl -X PUT http://localhost:5001/slots/doorbell \
|
|
225
235
|
-H 'X-API-Key: YOUR-KEY' -H 'Content-Type: application/json' \
|
|
226
236
|
-d '{"message": "<amber>Someone at the door", "ttl_seconds": 300}'
|
|
227
237
|
```
|
|
228
238
|
|
|
239
|
+
Take a message off the display without giving up its slot, and put it back later:
|
|
240
|
+
|
|
241
|
+
```
|
|
242
|
+
curl -X PUT http://localhost:5001/slots/doorbell/active \
|
|
243
|
+
-H 'X-API-Key: YOUR-KEY' -H 'Content-Type: application/json' \
|
|
244
|
+
-d '{"active": false}'
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
A hidden message keeps its slot, its place in the order and its text, so showing it again
|
|
248
|
+
takes `{"active": true}` and no copy of what it said. Hiding or showing one is a single run
|
|
249
|
+
sequence write. That does disturb the display briefly, but far less than rewriting a
|
|
250
|
+
message does: enough less that it is easy to miss unless you are watching for it on a
|
|
251
|
+
static screen. The exception is the last message showing: its file is emptied as it goes,
|
|
252
|
+
because a sign whose run sequence names nothing freezes on what it was drawing, so
|
|
253
|
+
putting that one back costs the text as well as the sequence.
|
|
254
|
+
|
|
255
|
+
`active` is a field on the message endpoint too, where leaving it out is the point: a
|
|
256
|
+
source re-sending the same content every few minutes says nothing about it and so cannot
|
|
257
|
+
switch back on something that was deliberately hidden. Sending it moves the message.
|
|
258
|
+
|
|
259
|
+
A `ttl_seconds` can hide a message instead of deleting it, which suits anything that comes
|
|
260
|
+
back later, such as a bin day or a school notice:
|
|
261
|
+
|
|
262
|
+
```
|
|
263
|
+
curl -X PUT http://localhost:5001/slots/bins \
|
|
264
|
+
-H 'X-API-Key: YOUR-KEY' -H 'Content-Type: application/json' \
|
|
265
|
+
-d '{"message": "<green>BINS OUT TONIGHT", "ttl_seconds": 43200,
|
|
266
|
+
"delete_on_expiry": false}'
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Put those together and a recurring notification is one call per event, sent in the same
|
|
270
|
+
shape every time. This shows the alarm's state for a minute whenever it changes, then takes
|
|
271
|
+
it off the rotation until the next one:
|
|
272
|
+
|
|
273
|
+
```
|
|
274
|
+
curl -X PUT http://localhost:5001/slots/alarm \
|
|
275
|
+
-H 'X-API-Key: YOUR-KEY' -H 'Content-Type: application/json' \
|
|
276
|
+
-d '{"message": "<red>ALARM NOW <var:arm_state>", "ttl_seconds": 60,
|
|
277
|
+
"delete_on_expiry": false, "active": true}'
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
The deadline clears itself as it hides the message, so each event gets a fresh minute
|
|
281
|
+
rather than the message vanishing again on a deadline the last one left behind.
|
|
282
|
+
|
|
283
|
+
Show a live value. Create the variable first, then a message that calls it:
|
|
284
|
+
|
|
285
|
+
```
|
|
286
|
+
curl -X PUT http://localhost:5001/variables/temp \
|
|
287
|
+
-H 'X-API-Key: YOUR-KEY' -H 'Content-Type: application/json' \
|
|
288
|
+
-d '{"value": "72", "ttl_seconds": 1800, "stale_value": "--"}'
|
|
289
|
+
|
|
290
|
+
curl -X PUT http://localhost:5001/slots/weather \
|
|
291
|
+
-H 'X-API-Key: YOUR-KEY' -H 'Content-Type: application/json' \
|
|
292
|
+
-d '{"message": "Outside <var:temp><degree>F", "display_mode": "ROTATE"}'
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
From then on only the variable needs writing, and the message picks each new value up
|
|
296
|
+
on its next pass across the sign.
|
|
297
|
+
|
|
229
298
|
Take the sign over for thirty seconds:
|
|
230
299
|
|
|
231
300
|
```
|
|
@@ -252,8 +321,8 @@ Silence it with `{"command": "SPEAKER", "parameter": "OFF"}`, and turn it back o
|
|
|
252
321
|
lives on the sign and survives a restart, so it is also the first thing to check if
|
|
253
322
|
`SOUND` ever seems to do nothing.
|
|
254
323
|
|
|
255
|
-
The full API is at `/docs`. Every markup token, display mode and
|
|
256
|
-
command is listed by the `/enumerations` reads there, which answer at
|
|
324
|
+
The full API is at `/docs`. Every markup token, value token, display mode and
|
|
325
|
+
control command is listed by the `/enumerations` reads there, which answer at
|
|
257
326
|
request time rather than being frozen into the description.
|
|
258
327
|
|
|
259
328
|
### Writing messages
|
|
@@ -267,6 +336,59 @@ displays correctly. A character the sign cannot render is rejected with a 400, a
|
|
|
267
336
|
unknown token: a write is told what the sign would have made of it rather than being
|
|
268
337
|
shown something it did not ask for.
|
|
269
338
|
|
|
339
|
+
### Live values: variables
|
|
340
|
+
|
|
341
|
+
A variable lives in a small file of its own on the sign, and a message or an alert
|
|
342
|
+
calls it with `<var:name>`. Writing a new value rewrites that file and nothing else,
|
|
343
|
+
which the sign takes without blanking, so a message showing a temperature or a count can
|
|
344
|
+
change every minute and never restart. In a scrolling mode the new value appears on the
|
|
345
|
+
message's next pass. An alert carrying a live wind speed works the same way, and keeps
|
|
346
|
+
the sign while the number changes.
|
|
347
|
+
|
|
348
|
+
A few things are worth knowing, all of them measured on the sign:
|
|
349
|
+
|
|
350
|
+
- **Create the variable before a message calls it.** A message or an alert naming a
|
|
351
|
+
variable that does not exist is refused with a 400, and a variable that a message or
|
|
352
|
+
the alert still calls cannot be deleted: that is a 409 naming what calls it.
|
|
353
|
+
- **Formatting in a value carries on after it.** A value of `<red>DOWN` turns the rest
|
|
354
|
+
of the message red as well, and so does a character set or a speed. If the text after
|
|
355
|
+
the call matters, set it again in the message after the call.
|
|
356
|
+
- **A value takes the message markup, less two tokens.** `<week_day>` draws as a literal
|
|
357
|
+
`9` from inside a variable, and a variable cannot call another. `GET
|
|
358
|
+
/enumerations/value-tokens` lists what is allowed.
|
|
359
|
+
- **Keep a changing number from pushing the text around it.** With the sign's usual
|
|
360
|
+
proportional spacing, `11` and `88` are different widths and the text after them moves.
|
|
361
|
+
Put `<fixed_width>` in the message before the call and send values of the same length.
|
|
362
|
+
Fixed width also left-justifies the line.
|
|
363
|
+
- **A value that stops arriving can go stale.** Give a `ttl_seconds` and a `stale_value`
|
|
364
|
+
such as `--`, and once the time passes without a new value the sign shows the stale
|
|
365
|
+
value instead. The variable itself stays, since messages call it.
|
|
366
|
+
- **A value that does not fit is refused.** Each holds `variable_capacity` bytes after
|
|
367
|
+
rendering, 32 by default. The sign does not cut an overlong value short, it empties it,
|
|
368
|
+
so the service refuses one rather than send it.
|
|
369
|
+
|
|
370
|
+
A Home Assistant `rest_command` that keeps a sensor on the sign:
|
|
371
|
+
|
|
372
|
+
```yaml
|
|
373
|
+
rest_command:
|
|
374
|
+
sign_temperature:
|
|
375
|
+
url: http://readerboard.local:5001/variables/temp
|
|
376
|
+
method: put
|
|
377
|
+
headers:
|
|
378
|
+
X-API-Key: !secret readerboard_key
|
|
379
|
+
content_type: application/json
|
|
380
|
+
payload: '{"value": "{{ value }}", "ttl_seconds": 1800, "stale_value": "--"}'
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
Call it from an automation triggered by the sensor, with the reading as `value`:
|
|
384
|
+
|
|
385
|
+
```yaml
|
|
386
|
+
actions:
|
|
387
|
+
- action: rest_command.sign_temperature
|
|
388
|
+
data:
|
|
389
|
+
value: "{{ states('sensor.outside_temperature') | round(0) | int }}"
|
|
390
|
+
```
|
|
391
|
+
|
|
270
392
|
### Recovering a sign that has stopped responding
|
|
271
393
|
|
|
272
394
|
A sign mounted out of reach can wedge: a stray bit corrupts what its decoder is
|
|
@@ -295,10 +417,10 @@ from the service's own record, so the display still comes back to what it was.
|
|
|
295
417
|
curl -X POST http://localhost:5001/sign/reboot -H 'X-API-Key: YOUR-KEY'
|
|
296
418
|
```
|
|
297
419
|
|
|
298
|
-
Reach for it only when a soft reset was not enough. The sign is blank for
|
|
299
|
-
|
|
300
|
-
that without resetting anything. The client
|
|
301
|
-
confirmation for the same reason.
|
|
420
|
+
Reach for it only when a soft reset was not enough. The sign is blank for twelve seconds
|
|
421
|
+
or more while it resets, longer with a lot of messages to put back. Neither is a way to
|
|
422
|
+
clear messages: `DELETE /slots` does that without resetting anything. The client
|
|
423
|
+
fronts the reboot with a warning-coloured confirmation for the same reason.
|
|
302
424
|
|
|
303
425
|
## Configuration
|
|
304
426
|
|
|
@@ -317,9 +439,11 @@ telnet serial protocol, `/dev/ttyUSB0` or `COM3` for a cable plugged straight in
|
|
|
317
439
|
and the port, and pyserial's answer to one that has a slash names neither the setting
|
|
318
440
|
nor the value.
|
|
319
441
|
|
|
320
|
-
|
|
321
|
-
on it**: `slot_count` and `
|
|
322
|
-
the log, but they are not settings to fiddle
|
|
442
|
+
Four settings reallocate the sign's memory when changed, and **that erases every message
|
|
443
|
+
on it**: `slot_count`, `slot_capacity`, `variable_count` and `variable_capacity`. The
|
|
444
|
+
service will do it, and say so loudly in the log, but they are not settings to fiddle
|
|
445
|
+
with. With `variable_count` at 0, `variable_capacity` allocates nothing, so changing it
|
|
446
|
+
alone reallocates nothing either.
|
|
323
447
|
|
|
324
448
|
## Security
|
|
325
449
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "readerboard"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.0"
|
|
8
8
|
description = "An HTTP service for BetaBrite and Alpha protocol LED signs: several sources share one sign, with alerts, expiring messages and clock sync"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -28,7 +28,7 @@ from readerboard.api.models import HealthResponse, LinkHealth
|
|
|
28
28
|
from readerboard.config import Settings
|
|
29
29
|
from readerboard.services.alerts import AlertService
|
|
30
30
|
from readerboard.services.clock import ClockService
|
|
31
|
-
from readerboard.services.registry import
|
|
31
|
+
from readerboard.services.registry import SlotRegistry
|
|
32
32
|
from readerboard.sign.controller import SignController
|
|
33
33
|
from readerboard.sign.layout import Layout
|
|
34
34
|
from readerboard.sign.state import StateStore
|
|
@@ -42,8 +42,14 @@ Drives a BetaBrite Classic sign over the Alpha protocol, either through a serial
|
|
|
42
42
|
cable or through an Ethernet to RS-232 adapter.
|
|
43
43
|
|
|
44
44
|
Several sources can share the sign at once. Each registers a named **slot**, and
|
|
45
|
-
the sign rotates through the
|
|
46
|
-
|
|
45
|
+
the sign rotates through the slots that are showing by itself. A slot can be
|
|
46
|
+
hidden without being given up, so a message can be taken off the display and put
|
|
47
|
+
back without being sent again. An **alert** takes the whole display over until it
|
|
48
|
+
is released, then the rotation resumes.
|
|
49
|
+
|
|
50
|
+
A **variable** is a value a slot's message or an alert calls with `<var:name>`.
|
|
51
|
+
Changing it rewrites only the variable, so the sign shows the new value without
|
|
52
|
+
blanking or restarting what calls it.
|
|
47
53
|
|
|
48
54
|
Every write needs an `X-API-Key` header, and so does `GET /sign/information`,
|
|
49
55
|
which reads the sign rather than the service: it puts a question on the wire and
|
|
@@ -53,13 +59,14 @@ holds the sign until the answer comes back. The service's own reads and
|
|
|
53
59
|
|
|
54
60
|
A failure is reported by the status code, with the reason in a `detail` field:
|
|
55
61
|
400 for a command the sign does not have, a parameter it will not accept, a
|
|
56
|
-
message too long for its
|
|
57
|
-
missing or wrong `X-API-Key`, 404
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
message or value too long for its file, markup the sign cannot render or a call
|
|
63
|
+
to a variable that does not exist, 401 for a missing or wrong `X-API-Key`, 404
|
|
64
|
+
for a slot or variable that does not exist, 409 when every slot or every
|
|
65
|
+
variable is already in use or a variable something still calls is deleted, 503
|
|
66
|
+
when the sign is unreachable, stops partway through an answer or answers with
|
|
67
|
+
something the service cannot read, or no API key is configured at all, 500 for
|
|
68
|
+
something the service has no code for, and 422 for a body that is not the shape
|
|
69
|
+
the endpoint declares, which includes a display mode the sign does not have.
|
|
63
70
|
"""
|
|
64
71
|
|
|
65
72
|
|
|
@@ -77,10 +84,10 @@ def build_transport(settings: Settings) -> Transport:
|
|
|
77
84
|
async def _refresh_loop(app: FastAPI, interval: float) -> None:
|
|
78
85
|
"""Push everything to the sign again, periodically.
|
|
79
86
|
|
|
80
|
-
See ``
|
|
87
|
+
See ``SlotRegistry.refresh`` for why blind re-pushing is the only thing
|
|
81
88
|
that repairs a sign power cycled behind a still-connected adapter.
|
|
82
89
|
"""
|
|
83
|
-
registry:
|
|
90
|
+
registry: SlotRegistry = app.state.registry
|
|
84
91
|
alerts: AlertService = app.state.alerts
|
|
85
92
|
|
|
86
93
|
while True:
|
|
@@ -99,8 +106,8 @@ async def _refresh_loop(app: FastAPI, interval: float) -> None:
|
|
|
99
106
|
|
|
100
107
|
|
|
101
108
|
async def _sweep_loop(app: FastAPI, interval: float) -> None:
|
|
102
|
-
"""Expire slots and alerts
|
|
103
|
-
registry:
|
|
109
|
+
"""Expire slots and alerts, and turn variables stale, once their deadlines pass."""
|
|
110
|
+
registry: SlotRegistry = app.state.registry
|
|
104
111
|
alerts: AlertService = app.state.alerts
|
|
105
112
|
|
|
106
113
|
while True:
|
|
@@ -137,14 +144,17 @@ def create_app(settings: Settings | None = None, transport: Transport | None = N
|
|
|
137
144
|
)
|
|
138
145
|
store = StateStore(settings.state_path)
|
|
139
146
|
state = store.load()
|
|
140
|
-
layout = Layout(
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
147
|
+
layout = Layout(
|
|
148
|
+
settings.slot_count,
|
|
149
|
+
settings.slot_capacity,
|
|
150
|
+
settings.variable_count,
|
|
151
|
+
settings.variable_capacity,
|
|
144
152
|
)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
153
|
+
alerts = AlertService(controller, store, state)
|
|
154
|
+
registry = SlotRegistry(controller, layout, store, state)
|
|
155
|
+
# An alert calling a variable is rendered by the registry, under its
|
|
156
|
+
# lock, so the variable cannot be deleted while the alert calls it.
|
|
157
|
+
alerts.set_rendering(registry.rendering)
|
|
148
158
|
clock = ClockService(
|
|
149
159
|
controller,
|
|
150
160
|
interval_seconds=settings.clock_sync_interval_seconds,
|
|
@@ -240,6 +250,7 @@ def create_app(settings: Settings | None = None, transport: Transport | None = N
|
|
|
240
250
|
clock = get_clock(request)
|
|
241
251
|
|
|
242
252
|
used, total = registry.occupancy
|
|
253
|
+
variables_used, variables_total = registry.variable_occupancy
|
|
243
254
|
return HealthResponse(
|
|
244
255
|
status="ok" if controller.is_connected else "degraded",
|
|
245
256
|
version=__version__,
|
|
@@ -253,6 +264,8 @@ def create_app(settings: Settings | None = None, transport: Transport | None = N
|
|
|
253
264
|
),
|
|
254
265
|
slots_used=used,
|
|
255
266
|
slots_total=total,
|
|
267
|
+
variables_used=variables_used,
|
|
268
|
+
variables_total=variables_total,
|
|
256
269
|
sign_in_sync=registry.in_sync,
|
|
257
270
|
alert_active=alerts.active is not None,
|
|
258
271
|
clock_last_synced_at=clock.last_sync_at,
|
|
@@ -16,7 +16,7 @@ from fastapi.security import APIKeyHeader
|
|
|
16
16
|
|
|
17
17
|
from readerboard.services.alerts import AlertService
|
|
18
18
|
from readerboard.services.clock import ClockService
|
|
19
|
-
from readerboard.services.registry import
|
|
19
|
+
from readerboard.services.registry import SlotRegistry
|
|
20
20
|
from readerboard.sign.controller import SignController
|
|
21
21
|
|
|
22
22
|
API_KEY_HEADER = "X-API-Key"
|
|
@@ -55,9 +55,9 @@ def get_controller(request: Request) -> SignController:
|
|
|
55
55
|
return controller
|
|
56
56
|
|
|
57
57
|
|
|
58
|
-
def get_registry(request: Request) ->
|
|
58
|
+
def get_registry(request: Request) -> SlotRegistry:
|
|
59
59
|
"""Return the registered messages."""
|
|
60
|
-
registry:
|
|
60
|
+
registry: SlotRegistry = request.app.state.registry
|
|
61
61
|
return registry
|
|
62
62
|
|
|
63
63
|
|
|
@@ -107,7 +107,7 @@ def require_api_key(
|
|
|
107
107
|
|
|
108
108
|
|
|
109
109
|
ControllerDep = Annotated[SignController, Depends(get_controller)]
|
|
110
|
-
RegistryDep = Annotated[
|
|
110
|
+
RegistryDep = Annotated[SlotRegistry, Depends(get_registry)]
|
|
111
111
|
AlertsDep = Annotated[AlertService, Depends(get_alerts)]
|
|
112
112
|
ClockDep = Annotated[ClockService, Depends(get_clock)]
|
|
113
113
|
RequireApiKey = Depends(require_api_key)
|
|
@@ -18,7 +18,14 @@ from readerboard.protocol.markup import MarkupError
|
|
|
18
18
|
from readerboard.protocol.replies import ReplyError
|
|
19
19
|
from readerboard.services import commands
|
|
20
20
|
from readerboard.services.alerts import AlertTooLong
|
|
21
|
-
from readerboard.services.registry import
|
|
21
|
+
from readerboard.services.registry import (
|
|
22
|
+
MessageTooLong,
|
|
23
|
+
UnknownSlot,
|
|
24
|
+
UnknownVariable,
|
|
25
|
+
VariableInUse,
|
|
26
|
+
VariablesDisabled,
|
|
27
|
+
VariableTooLong,
|
|
28
|
+
)
|
|
22
29
|
from readerboard.sign.layout import LayoutFull
|
|
23
30
|
from readerboard.transport.base import TransportError
|
|
24
31
|
|
|
@@ -26,11 +33,18 @@ STATUS_FOR_ERROR: tuple[tuple[type[Exception], int], ...] = (
|
|
|
26
33
|
(MarkupError, status.HTTP_400_BAD_REQUEST),
|
|
27
34
|
(ProtocolError, status.HTTP_400_BAD_REQUEST),
|
|
28
35
|
(MessageTooLong, status.HTTP_400_BAD_REQUEST),
|
|
36
|
+
(VariableTooLong, status.HTTP_400_BAD_REQUEST),
|
|
37
|
+
(VariablesDisabled, status.HTTP_400_BAD_REQUEST),
|
|
29
38
|
(AlertTooLong, status.HTTP_400_BAD_REQUEST),
|
|
30
39
|
(commands.UnknownCommand, status.HTTP_400_BAD_REQUEST),
|
|
31
40
|
(commands.BadParameter, status.HTTP_400_BAD_REQUEST),
|
|
32
41
|
(UnknownSlot, status.HTTP_404_NOT_FOUND),
|
|
42
|
+
(UnknownVariable, status.HTTP_404_NOT_FOUND),
|
|
33
43
|
(LayoutFull, status.HTTP_409_CONFLICT),
|
|
44
|
+
# Deleting a variable a message still calls would leave that message
|
|
45
|
+
# calling a file the next variable could be given. Not the caller's
|
|
46
|
+
# request being malformed, which is what makes it a conflict.
|
|
47
|
+
(VariableInUse, status.HTTP_409_CONFLICT),
|
|
34
48
|
(TransportError, status.HTTP_503_SERVICE_UNAVAILABLE),
|
|
35
49
|
# A sign that answers with something unreadable is as unusable as one
|
|
36
50
|
# that does not answer, and neither is the caller's doing. The route
|