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.
@@ -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 topics.yaml modules.yaml; do
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
@@ -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