frugal-iot-server 0.3.7 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/INSTALLATION.md +299 -0
- package/README.md +94 -19
- package/config.d/schema/devices.yaml +63 -0
- package/config.d/schema/modules.yaml +8 -37
- package/config.d/schema/topics.yaml +39 -6
- package/extras/mosquitto-bridge.conf.example +130 -0
- package/frugal-iot-server.js +50 -2
- package/lib/housekeeping.js +10 -4
- package/package.json +10 -5
- package/public/index.html +8 -0
- package/public/service-worker.js +4 -3
- package/scripts/addbridge-pi.zsh +164 -0
- package/scripts/addbridge-prod.zsh +215 -0
- package/scripts/check-published-files.js +0 -0
- package/scripts/check-schema.js +126 -15
- package/scripts/clearretained.zsh +153 -0
- package/scripts/copy-schema-to-examples.zsh +9 -5
- package/scripts/diagnostic.zsh +47 -0
- package/scripts/install-pi.sh +646 -0
- package/scripts/not-published.txt +27 -0
- package/scripts/prerelease.zsh +91 -17
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
#!/usr/bin/env zsh
|
|
2
|
+
#
|
|
3
|
+
# List, and optionally delete, retained MQTT messages.
|
|
4
|
+
#
|
|
5
|
+
# Nodes publish nearly everything retained, so the broker keeps the last value of every topic and
|
|
6
|
+
# hands it to each new subscriber - which is how a dashboard can show a reading straight away
|
|
7
|
+
# instead of waiting for the next one. The cost is that a topic published by mistake outlives the
|
|
8
|
+
# mistake: rename a module, misspell a field, test a node with the wrong id, and that topic stays
|
|
9
|
+
# on the broker for ever, appearing on every dashboard. Restarting the broker does not clear it and
|
|
10
|
+
# neither does fixing the node. The only way to remove one is to publish an empty message to that
|
|
11
|
+
# exact topic, which is what this does.
|
|
12
|
+
#
|
|
13
|
+
# Run from the server's own directory, the one holding frugal-iot.db, so that the organization's
|
|
14
|
+
# broker password can be read from config.d/organizations/.
|
|
15
|
+
#
|
|
16
|
+
# Usage:
|
|
17
|
+
# npx --no frugal-iot-clearretained '<topic-pattern>' # list what is retained
|
|
18
|
+
# npx --no frugal-iot-clearretained '<topic-pattern>' --delete # delete it
|
|
19
|
+
# scripts/clearretained.zsh '<topic-pattern>' [--delete]
|
|
20
|
+
#
|
|
21
|
+
# Quote the pattern, or the shell expands the wildcards before this ever sees them. In MQTT
|
|
22
|
+
# patterns "+" matches exactly one level and "#" matches the rest:
|
|
23
|
+
# 'myfarm/#' everything the organization holds - look before deleting
|
|
24
|
+
# 'myfarm/lotus/esp8266-fb94bb/#' one node
|
|
25
|
+
# 'myfarm/lotus/+/sht/temperture/#' a misspelled field, on every node in the project
|
|
26
|
+
#
|
|
27
|
+
# It lists by default and deletes only when told to, because there is no undo. A node's min, max,
|
|
28
|
+
# colour and wiring are retained messages too, and they are how the dashboard knows how to draw it
|
|
29
|
+
# - delete those and the node has to be restarted before it looks right again.
|
|
30
|
+
|
|
31
|
+
set -euo pipefail
|
|
32
|
+
|
|
33
|
+
SCRIPT_NAME=$0
|
|
34
|
+
|
|
35
|
+
usage() {
|
|
36
|
+
echo "Usage: ${SCRIPT_NAME} '<topic-pattern>' [--delete] [-h host] [-p port] [-u user] [-P password]" >&2
|
|
37
|
+
echo "Example: ${SCRIPT_NAME} 'myfarm/lotus/+/sht/temperture/#'" >&2
|
|
38
|
+
echo " ${SCRIPT_NAME} 'myfarm/lotus/+/sht/temperture/#' --delete" >&2
|
|
39
|
+
exit 1
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if [[ $# -lt 1 ]]; then
|
|
43
|
+
usage
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
PATTERN=$1
|
|
47
|
+
shift
|
|
48
|
+
|
|
49
|
+
# Deliberately not called USERNAME and HOST: zsh makes both of those special parameters, tied to
|
|
50
|
+
# the real user and machine, so assigning to them does not do what it looks like. An earlier
|
|
51
|
+
# version of this script connected to the broker as the login user instead of as the organization,
|
|
52
|
+
# and the only symptom was "not authorised".
|
|
53
|
+
DELETE=false
|
|
54
|
+
BROKER_HOST=localhost
|
|
55
|
+
BROKER_PORT=1883
|
|
56
|
+
BROKER_USER=""
|
|
57
|
+
PASSWORD=""
|
|
58
|
+
# How long to wait for the broker to send what it has. Retained messages arrive immediately on
|
|
59
|
+
# subscribing, so this only has to cover the round trip; a busy broker on a slow Pi may want more.
|
|
60
|
+
WAIT=3
|
|
61
|
+
|
|
62
|
+
while [[ $# -gt 0 ]]; do
|
|
63
|
+
case "$1" in
|
|
64
|
+
--delete) DELETE=true; shift ;;
|
|
65
|
+
-h) BROKER_HOST=${2:-}; shift 2 ;;
|
|
66
|
+
-p) BROKER_PORT=${2:-}; shift 2 ;;
|
|
67
|
+
-u) BROKER_USER=${2:-}; shift 2 ;;
|
|
68
|
+
-P) PASSWORD=${2:-}; shift 2 ;;
|
|
69
|
+
-w) WAIT=${2:-}; shift 2 ;;
|
|
70
|
+
*) echo "Error: unexpected argument '$1'" >&2; usage ;;
|
|
71
|
+
esac
|
|
72
|
+
done
|
|
73
|
+
|
|
74
|
+
if ! command -v mosquitto_sub >/dev/null || ! command -v mosquitto_pub >/dev/null; then
|
|
75
|
+
echo "Error: mosquitto_sub and mosquitto_pub are needed (sudo apt install mosquitto-clients)" >&2
|
|
76
|
+
exit 1
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# A bare "#" would be every topic on the broker, across every organization. Almost always a
|
|
80
|
+
# mistake, and the one case where a slip is unrecoverable, so it is not accepted at all.
|
|
81
|
+
if [[ "$PATTERN" == "#" || "$PATTERN" == "+/#" ]]; then
|
|
82
|
+
echo "Error: refusing the pattern '${PATTERN}' - that is every topic on the broker." >&2
|
|
83
|
+
echo "Name the organization at least, as in 'myfarm/#'." >&2
|
|
84
|
+
exit 1
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
# Credentials. The broker requires them, and the organization's are in its config file - the same
|
|
88
|
+
# place the server reads them from. The first element of the topic pattern is the organization.
|
|
89
|
+
if [[ -z "$BROKER_USER" ]]; then
|
|
90
|
+
BROKER_USER=${PATTERN%%/*}
|
|
91
|
+
CONFIG_FILE="config.d/organizations/${BROKER_USER}.yaml"
|
|
92
|
+
if [[ ! -f "$CONFIG_FILE" ]]; then
|
|
93
|
+
echo "Error: no ${CONFIG_FILE} here, so the broker password for '${BROKER_USER}' cannot be found." >&2
|
|
94
|
+
echo "Run this from the server's own directory, or give credentials with -u and -P." >&2
|
|
95
|
+
exit 1
|
|
96
|
+
fi
|
|
97
|
+
PASSWORD=$(sed -n 's/^mqtt_password:[[:space:]]*//p' "$CONFIG_FILE" | head -1 | tr -d '"'"'"'')
|
|
98
|
+
if [[ -z "$PASSWORD" ]]; then
|
|
99
|
+
echo "Error: no mqtt_password in ${CONFIG_FILE}" >&2
|
|
100
|
+
exit 1
|
|
101
|
+
fi
|
|
102
|
+
fi
|
|
103
|
+
|
|
104
|
+
# What is actually retained. "--retained-only" ignores anything published while we are listening,
|
|
105
|
+
# so a node reporting normally is not mistaken for a leftover, and "-F %t" prints just the topic,
|
|
106
|
+
# which matters because a payload can itself contain spaces or newlines.
|
|
107
|
+
echo "Looking for retained messages matching '${PATTERN}' on ${BROKER_HOST}:${BROKER_PORT} as ${BROKER_USER} ..."
|
|
108
|
+
set +e
|
|
109
|
+
TOPICS=$(mosquitto_sub -h "$BROKER_HOST" -p "$BROKER_PORT" -u "$BROKER_USER" -P "$PASSWORD" \
|
|
110
|
+
--retained-only -F '%t' -t "$PATTERN" -W "$WAIT" 2>&1)
|
|
111
|
+
set -e
|
|
112
|
+
|
|
113
|
+
# -W always ends in a timeout, which is not a failure here, but a refused login is
|
|
114
|
+
if print -r -- "$TOPICS" | grep -qi "not authorised\|Connection refused\|Connection error"; then
|
|
115
|
+
echo "Error: the broker refused the connection:" >&2
|
|
116
|
+
print -r -- "$TOPICS" | grep -i "not authorised\|Connection refused\|Connection error" >&2
|
|
117
|
+
exit 1
|
|
118
|
+
fi
|
|
119
|
+
# "|| true" because grep exits non-zero when it filters everything out, and under "set -e" that
|
|
120
|
+
# would end the script here - silently, before it could say that nothing matched.
|
|
121
|
+
TOPICS=$(print -r -- "$TOPICS" | grep -v '^Timed out$' | grep -v '^$' | sort -u) || true
|
|
122
|
+
|
|
123
|
+
if [[ -z "$TOPICS" ]]; then
|
|
124
|
+
echo "Nothing retained matches that pattern."
|
|
125
|
+
exit 0
|
|
126
|
+
fi
|
|
127
|
+
|
|
128
|
+
COUNT=$(print -r -- "$TOPICS" | grep -c .) || true
|
|
129
|
+
print -r -- "$TOPICS" | sed 's/^/ /'
|
|
130
|
+
echo "${COUNT} retained topic(s)."
|
|
131
|
+
|
|
132
|
+
if [[ "$DELETE" != true ]]; then
|
|
133
|
+
echo ""
|
|
134
|
+
echo "Nothing has been changed. To delete these, run the same command again with --delete"
|
|
135
|
+
exit 0
|
|
136
|
+
fi
|
|
137
|
+
|
|
138
|
+
# Deleting one means publishing a zero-length retained message to it, which is how MQTT spells
|
|
139
|
+
# "forget this". Sent at QoS 1 so the broker acknowledges each one rather than dropping it under load.
|
|
140
|
+
echo "Deleting ${COUNT} retained topic(s) ..."
|
|
141
|
+
print -r -- "$TOPICS" | while IFS= read -r t; do
|
|
142
|
+
if mosquitto_pub -h "$BROKER_HOST" -p "$BROKER_PORT" -u "$BROKER_USER" -P "$PASSWORD" -q 1 -r -n -t "$t"; then
|
|
143
|
+
echo " cleared ${t}"
|
|
144
|
+
else
|
|
145
|
+
echo " FAILED ${t}" >&2
|
|
146
|
+
fi
|
|
147
|
+
done
|
|
148
|
+
|
|
149
|
+
echo ""
|
|
150
|
+
echo "Done. Check with the same command without --delete - it should report nothing."
|
|
151
|
+
echo "If a topic comes back, a node is still publishing it: fix or restart the node, then re-run."
|
|
152
|
+
# A bridged broker forwards these deletions to the other end, so clearing here clears there too.
|
|
153
|
+
exit 0
|
|
@@ -32,13 +32,17 @@ if (( ! ${#DIRS} )); then
|
|
|
32
32
|
exit 0
|
|
33
33
|
fi
|
|
34
34
|
|
|
35
|
+
# Every yaml in the schema directory, rather than a list to keep in step - devices.yaml was added
|
|
36
|
+
# and left behind once already.
|
|
37
|
+
FILES=("${SCHEMA}"/*.yaml(N:t))
|
|
38
|
+
if (( ! ${#FILES} )); then
|
|
39
|
+
echo " No yaml files in ${SCHEMA}"
|
|
40
|
+
exit 0
|
|
41
|
+
fi
|
|
42
|
+
|
|
35
43
|
COPIED=0
|
|
36
44
|
for d in $DIRS; do
|
|
37
|
-
for f in
|
|
38
|
-
if [[ ! -f "${SCHEMA}/${f}" ]]; then
|
|
39
|
-
echo " ${SCHEMA}/${f} does not exist - skipped"
|
|
40
|
-
continue
|
|
41
|
-
fi
|
|
45
|
+
for f in $FILES; do
|
|
42
46
|
if cmp -s "${SCHEMA}/${f}" "${d}/${f}"; then
|
|
43
47
|
echo " same ${d#${LOGGER}/}/${f}"
|
|
44
48
|
else
|
package/scripts/diagnostic.zsh
CHANGED
|
@@ -266,6 +266,53 @@ else
|
|
|
266
266
|
item "mosquitto_sub not installed - cannot test broker logins (sudo apt install mosquitto-clients)"
|
|
267
267
|
fi
|
|
268
268
|
|
|
269
|
+
section "Bridge to a production server (optional)"
|
|
270
|
+
# A bridge relays this Pi's readings to a production server - see
|
|
271
|
+
# extras/mosquitto-bridge.conf.example. It is configured by a "connection" line in one of
|
|
272
|
+
# mosquitto's own config files, so most installations have none, which is not a problem.
|
|
273
|
+
BRIDGECONFS=(/etc/mosquitto/mosquitto.conf(N) /etc/mosquitto/conf.d/*.conf(N))
|
|
274
|
+
BRIDGENAMES=""
|
|
275
|
+
(( ${#BRIDGECONFS} )) && BRIDGENAMES=$(grep -hE '^[[:space:]]*connection[[:space:]]' $BRIDGECONFS 2>/dev/null | awk '{print $2}')
|
|
276
|
+
if [[ -z "$BRIDGENAMES" ]]; then
|
|
277
|
+
item "no bridge configured - readings stay on this Pi"
|
|
278
|
+
else
|
|
279
|
+
for b in ${(f)BRIDGENAMES}; do item "configured bridge: $b"; done
|
|
280
|
+
# What it is set to relay and where. remote_password is deliberately not among these.
|
|
281
|
+
item "its settings:"
|
|
282
|
+
grep -hE '^[[:space:]]*(address|remote_username|remote_clientid|topic|cleansession|restart_timeout|notifications)[[:space:]]' $BRIDGECONFS 2>/dev/null | sed 's/^/ /'
|
|
283
|
+
# Whether it is actually up, rather than what the configuration hoped for. With "notifications"
|
|
284
|
+
# left on, the bridge publishes a retained 1 or 0 here each time it connects or drops.
|
|
285
|
+
if have mosquitto_sub; then
|
|
286
|
+
# Reading $SYS needs a broker login, so borrow the first organization that has one
|
|
287
|
+
STATEORG=""; STATEPW=""
|
|
288
|
+
for f in config.d/organizations/*.yaml(N); do
|
|
289
|
+
STATEPW=$(sed -n 's/^mqtt_password:[[:space:]]*//p' "$f" 2>/dev/null | head -1 | tr -d '"'"'"'')
|
|
290
|
+
if [[ -n "$STATEPW" ]]; then STATEORG=${f:t:r}; break; fi
|
|
291
|
+
done
|
|
292
|
+
if [[ -z "$STATEORG" ]]; then
|
|
293
|
+
item "live state: no organization credentials here to read it with"
|
|
294
|
+
else
|
|
295
|
+
STATEOUT=$(mosquitto_sub -h localhost -u "$STATEORG" -P "$STATEPW" -v -t '$SYS/broker/connection/+/state' -W 2 2>&1 | grep -E '/state ')
|
|
296
|
+
if [[ -z "$STATEOUT" ]]; then
|
|
297
|
+
item "live state: the broker reported nothing - the bridge has never connected since it started"
|
|
298
|
+
problem "A bridge is configured but has not reported its state - check 'address' is reachable and see the mosquitto log above"
|
|
299
|
+
else
|
|
300
|
+
for line in ${(f)STATEOUT}; do
|
|
301
|
+
BNAME=${${(s:/:)${line%% *}}[4]}
|
|
302
|
+
if [[ "${line##* }" == "1" ]]; then
|
|
303
|
+
item "live state: ${BNAME} is CONNECTED to production"
|
|
304
|
+
else
|
|
305
|
+
item "live state: ${BNAME} is DOWN - this Pi is recording locally only"
|
|
306
|
+
problem "Bridge '${BNAME}' is not connected to production - readings since it dropped will not appear there"
|
|
307
|
+
fi
|
|
308
|
+
done
|
|
309
|
+
fi
|
|
310
|
+
fi
|
|
311
|
+
else
|
|
312
|
+
item "mosquitto_sub not installed - cannot check whether the bridge is up"
|
|
313
|
+
fi
|
|
314
|
+
fi
|
|
315
|
+
|
|
269
316
|
section "Web server (step 7)"
|
|
270
317
|
WEBPORT=$(grep -h '^port:' config.d/server.yaml 2>/dev/null | awk '{print $2}')
|
|
271
318
|
[[ -z "$WEBPORT" ]] && WEBPORT=8080
|