frugal-iot-server 0.3.7 → 0.3.9
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 +398 -0
- package/README.md +94 -19
- package/config.d/email.yaml +23 -0
- package/config.d/schema/devices.yaml +61 -0
- package/config.d/schema/modules.yaml +337 -39
- package/config.d/schema/topics.yaml +156 -52
- package/extras/mosquitto-bridge.conf.example +130 -0
- package/frugal-iot-server.js +190 -10
- package/lib/housekeeping.js +10 -4
- package/lib/mailer.js +43 -0
- package/lib/resetcode.js +103 -0
- package/package.json +13 -5
- package/public/index.html +16 -0
- package/public/service-worker.js +29 -29
- package/scripts/addbridge-pi.zsh +194 -0
- package/scripts/addbridge-prod.zsh +221 -0
- package/scripts/check-published-files.js +0 -0
- package/scripts/check-schema.js +126 -15
- package/scripts/clearretained.js +185 -0
- package/scripts/copy-schema-to-examples.zsh +9 -5
- package/scripts/diagnostic.zsh +47 -0
- package/scripts/init.zsh +1 -1
- package/scripts/install-pi.sh +646 -0
- package/scripts/not-published.txt +27 -0
- package/scripts/prerelease.zsh +91 -17
package/INSTALLATION.md
CHANGED
|
@@ -40,6 +40,8 @@ Any of these work. What differs is only how long the install takes, and how much
|
|
|
40
40
|
A Zero W runs the server perfectly well once installed. It is *installing* that is slow, because a
|
|
41
41
|
single 1 GHz core has to unpack several hundred packages and compile one of them.
|
|
42
42
|
|
|
43
|
+
|
|
44
|
+
---
|
|
43
45
|
### 0. What you need before you start
|
|
44
46
|
|
|
45
47
|
**Hardware**
|
|
@@ -125,6 +127,7 @@ From your laptop's terminal:
|
|
|
125
127
|
```
|
|
126
128
|
ssh pi@frugaliot.local
|
|
127
129
|
```
|
|
130
|
+
> **Pi Zero W:** This can take a long time to connect, try `ping frugaliot.local` first to check it is alive
|
|
128
131
|
|
|
129
132
|
Say `yes` to the fingerprint question, then give the password you set in Imager.
|
|
130
133
|
|
|
@@ -175,6 +178,44 @@ the password — either way `nmtui` is the fix, and it is not worth more time th
|
|
|
175
178
|
(`sudo nmcli device wifi list` shows each nearby network's SSID and whether it is WPA2 or WPA3;
|
|
176
179
|
WPA3 cannot use a derived key at all.)
|
|
177
180
|
|
|
181
|
+
## 3-8. The short way: one script
|
|
182
|
+
|
|
183
|
+
Steps 3 to 8 below can be done for you. Once you can ssh into the Pi (steps 1 and 2, which need a
|
|
184
|
+
person with an SD card):
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
curl -fSLO https://raw.githubusercontent.com/mitra42/frugal-iot-server/main/scripts/install-pi.sh
|
|
188
|
+
bash install-pi.sh --org myfarm --name "My Farm" --email you@example.com --phone +61123456789
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
It asks for anything it needs that you did not pass, says what it is doing as it goes, and stops at
|
|
192
|
+
the first thing that fails - leaving a log, and telling you to run `frugal-iot-diagnostic`. Running
|
|
193
|
+
it again after a failure carries on rather than starting over, which matters on a Pi Zero W where one
|
|
194
|
+
step takes about 40 minutes. `--dry-run` checks the arguments and shows the plan without touching the
|
|
195
|
+
machine.
|
|
196
|
+
|
|
197
|
+
**Passwords** are asked for, not invented, unless you say otherwise: it prompts for each (twice, not
|
|
198
|
+
echoed) and generates one if you just press Enter. Pass them as `--superuser-password`,
|
|
199
|
+
`--login-password` and `--broker-password` to skip the prompts, `--random-passwords` to have all
|
|
200
|
+
three generated, or `--yes` to ask nothing at all — which is what to use over ssh with no terminal.
|
|
201
|
+
A password you chose is not written to the log; a generated one has to be, since otherwise you would
|
|
202
|
+
have no way of knowing it.
|
|
203
|
+
|
|
204
|
+
**It tells you whether a reboot is needed**, rather than leaving you to wonder — it notices a kernel
|
|
205
|
+
or boot-firmware package in the upgrade, the images in `/boot` changing underneath it, and
|
|
206
|
+
`/var/run/reboot-required`. If none of those happened it says so, and there is nothing more to do.
|
|
207
|
+
|
|
208
|
+
Fetch it with `curl -O` and then run it, rather than piping curl into bash - piped, it has no
|
|
209
|
+
terminal to ask questions at.
|
|
210
|
+
|
|
211
|
+
It finishes with a live server and a tested broker. It cannot do step 9, pointing your nodes at it,
|
|
212
|
+
because a node learns its broker by being flashed with it - so it prints the settings to give them.
|
|
213
|
+
Nor step 10, HTTPS.
|
|
214
|
+
|
|
215
|
+
The steps below are what it does, in the same order, if you would rather do it by hand or need to
|
|
216
|
+
understand what went wrong, otherwise skip to Step 9.
|
|
217
|
+
|
|
218
|
+
|
|
178
219
|
### 3. Update the operating system and install the prerequisites
|
|
179
220
|
|
|
180
221
|
Logged in, bring the system up to date and reboot:
|
|
@@ -654,6 +695,101 @@ mosquitto_sub -h localhost -u myfarm -P '<broker-password>' -t '#' -v
|
|
|
654
695
|
Every reading from every node should scroll past. Seeing anything here also proves the broker's
|
|
655
696
|
port 1883 is reachable from off the Pi, which is what the nodes need.
|
|
656
697
|
|
|
698
|
+
### 9a. Optional: outgoing mail, so people can reset a forgotten password
|
|
699
|
+
|
|
700
|
+
Without this, a forgotten password can only be fixed by you, on the Pi, with
|
|
701
|
+
`npx --no frugal-iot-setpassword <username> <new-password>`. The login page's "Forgot password?"
|
|
702
|
+
link says "Password reset is not available on this server" rather than pretending to send anything.
|
|
703
|
+
|
|
704
|
+
An offline Pi cannot send mail at all, so skip this unless it has internet access.
|
|
705
|
+
|
|
706
|
+
Mail sent straight from a home broadband connection is almost always treated as spam, so this
|
|
707
|
+
relays through somebody else's SMTP server rather than sending directly. Gmail is the usual choice
|
|
708
|
+
and is written out in full below; any other provider works the same way.
|
|
709
|
+
|
|
710
|
+
#### With Gmail
|
|
711
|
+
|
|
712
|
+
Use a Google account you are willing to have the Pi send as. A separate account for the purpose is
|
|
713
|
+
better than your own mailbox — the password ends up in a file on an SD card in a shed.
|
|
714
|
+
|
|
715
|
+
**1. Turn on 2-Step Verification** on that account, at
|
|
716
|
+
<https://myaccount.google.com/signinoptions/twosv>.
|
|
717
|
+
|
|
718
|
+
This is not optional and it is where most people get stuck: **app passwords do not exist until
|
|
719
|
+
2-Step Verification is on**. Until then the page in step 2 simply says the setting is not available,
|
|
720
|
+
without explaining why.
|
|
721
|
+
|
|
722
|
+
**2. Create an app password** at <https://myaccount.google.com/apppasswords>.
|
|
723
|
+
|
|
724
|
+
Type a name for it — anything you will recognise later, such as `Frugal IoT Pi` — and create it.
|
|
725
|
+
Google shows you **16 lowercase letters, in four groups of four**, once. Copy them now; you cannot
|
|
726
|
+
come back and read it again, only delete it and make another.
|
|
727
|
+
|
|
728
|
+
The groups are only there to make it readable. **Type it into the config with the spaces removed**,
|
|
729
|
+
as one 16-character word.
|
|
730
|
+
|
|
731
|
+
**3. Fill in `config.d/email.yaml`:**
|
|
732
|
+
|
|
733
|
+
```yaml
|
|
734
|
+
host: smtp.gmail.com
|
|
735
|
+
port: 587
|
|
736
|
+
user: yourname@gmail.com
|
|
737
|
+
pass: "abcdefghijklmnop" # the 16 characters from step 2, spaces removed
|
|
738
|
+
from: Frugal IoT <yourname@gmail.com>
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
Things that catch people out with Gmail specifically:
|
|
742
|
+
|
|
743
|
+
* **`from:` must be the same address as `user:`.** Gmail rewrites the sender to the account you
|
|
744
|
+
authenticated as, so a different address here does not fail — it quietly arrives as something
|
|
745
|
+
else, which is harder to diagnose than an error. (An address you have set up under Gmail's
|
|
746
|
+
"Send mail as" is the one exception.)
|
|
747
|
+
* **`user:` is the full address**, including `@gmail.com`, not just the part before it.
|
|
748
|
+
* **`Username and Password not accepted`** in the server's log means the login password was used
|
|
749
|
+
instead of an app password. Gmail has not accepted account passwords over SMTP for years.
|
|
750
|
+
* **Port 465 also works** if 587 is blocked where the Pi is; the software works out the encryption
|
|
751
|
+
from the port number, so change nothing else.
|
|
752
|
+
* **A free Gmail account can send around 500 messages a day.** Password resets will not come close.
|
|
753
|
+
* **Google Workspace administrators can switch app passwords off** for a whole domain. If step 2
|
|
754
|
+
offers you nothing on a work account, that is why, and you will need a real SMTP relay instead.
|
|
755
|
+
|
|
756
|
+
#### With any other provider
|
|
757
|
+
|
|
758
|
+
The same four settings, with that provider's SMTP host:
|
|
759
|
+
|
|
760
|
+
```yaml
|
|
761
|
+
host: smtp.example.org
|
|
762
|
+
port: 587 # 465 is implicit TLS, 587 and 25 are STARTTLS
|
|
763
|
+
user: frugaliot@example.org
|
|
764
|
+
pass: an-app-password-not-your-login-password
|
|
765
|
+
from: Frugal IoT <frugaliot@example.org>
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
Fastmail, Zoho, Proton (via its bridge) and the rest all issue app passwords in much the same way,
|
|
769
|
+
and for the same reason: one leaking off a Pi then costs you one mailbox rather than the account.
|
|
770
|
+
|
|
771
|
+
#### Checking it, either way
|
|
772
|
+
|
|
773
|
+
Restart the server and look at its output:
|
|
774
|
+
|
|
775
|
+
* `Sending mail via smtp.gmail.com:587 as ...` — configured.
|
|
776
|
+
* `Not sending mail (no host/from in config.d/email.yaml) ...` — it did not find a `host` and a
|
|
777
|
+
`from`, so the reset link will say it is unavailable.
|
|
778
|
+
|
|
779
|
+
Then use "Forgot password?" on the login page with your own account. If the mail never arrives, the
|
|
780
|
+
reason is in the server's log — the page deliberately gives the same answer whether or not the
|
|
781
|
+
address is known to it, so it cannot be used to find out who has an account here.
|
|
782
|
+
|
|
783
|
+
Two more things worth knowing:
|
|
784
|
+
|
|
785
|
+
* `pass` sits in a file on the SD card in plain text. Give that mailbox nothing else to lose.
|
|
786
|
+
* If the Pi is behind a proxy that does not set `X-Forwarded-Host`, the link in the mail will point
|
|
787
|
+
at the wrong address. Set `baseurl: https://your.address` in the same file.
|
|
788
|
+
|
|
789
|
+
The reset code itself is stored nowhere — it is a hash of the account, its current password and the
|
|
790
|
+
time, valid between five and ten minutes and dead the moment it is used. There is no table to
|
|
791
|
+
maintain and nothing to clean up. Restarting the server invalidates any code already sent.
|
|
792
|
+
|
|
657
793
|
### 10. HTTPS and over-the-air firmware updates
|
|
658
794
|
|
|
659
795
|
**To be written.** Everything above gives you a plain HTTP server on your local network, which is
|
|
@@ -667,6 +803,238 @@ That section will cover: a DNS name for the Pi, port forwarding on the router, n
|
|
|
667
803
|
proxy in front of port 8080, and certificates from Let's Encrypt via certbot. Both require the Pi
|
|
668
804
|
to have internet access, which is the opposite of the offline case this guide is aimed at.
|
|
669
805
|
|
|
806
|
+
### 11. Optional: bridge this Pi to a production server
|
|
807
|
+
|
|
808
|
+
Everything above gives a self-contained Pi. This step also relays its readings to a production
|
|
809
|
+
Frugal IoT server, so the same nodes appear on a dashboard elsewhere and can be controlled from
|
|
810
|
+
it, while the Pi carries on working on its own whenever the link is down. Skip it if you do not
|
|
811
|
+
want that — nothing else depends on it.
|
|
812
|
+
|
|
813
|
+
The relaying is done by the broker, not by the server: Mosquitto has a "bridge" feature for
|
|
814
|
+
connecting to another broker, and the server, the logger and the nodes need no configuration
|
|
815
|
+
change at all. This installation ships an example bridge configuration in
|
|
816
|
+
`extras/mosquitto-bridge.conf.example`, which explains each setting in place; this section is the
|
|
817
|
+
surrounding work.
|
|
818
|
+
|
|
819
|
+
It comes in three parts, and only the first is a one-off:
|
|
820
|
+
|
|
821
|
+
* **11a — preparing a production server to accept bridges.** A TLS listener, a certificate, and an
|
|
822
|
+
access-control file. Done once for a given production server, and not again until there is
|
|
823
|
+
another one.
|
|
824
|
+
* **11b — authorizing this Pi on that server.** One command, run there, once per Pi.
|
|
825
|
+
* **11c — pointing this Pi at it.** One command, run here, once per Pi.
|
|
826
|
+
|
|
827
|
+
**What you get, and what you do not.** While the link is up, readings appear on production within
|
|
828
|
+
a second or so. While it is down, the Pi records everything as usual and production simply has a
|
|
829
|
+
gap — the readings taken during an outage never reach it. That is a deliberate choice: the
|
|
830
|
+
alternative is for the broker to hold them and deliver the backlog on reconnect, but production
|
|
831
|
+
timestamps each reading as it arrives, so a day's backlog would arrive claiming to have happened
|
|
832
|
+
in the instant the link returned. A gap is honest; that would not be. Controls are treated the
|
|
833
|
+
other way round, and *are* queued, so turning something on from production while the Pi is offline
|
|
834
|
+
takes effect when it reconnects.
|
|
835
|
+
|
|
836
|
+
#### 11a. Preparing a production server to accept bridges
|
|
837
|
+
|
|
838
|
+
Once per production server, not per Pi — and if bridging to `frugaliot.naturalinnovation.org`
|
|
839
|
+
this has already been done. If so, skip to 11b.
|
|
840
|
+
|
|
841
|
+
**Add a listener for bridges.** A Mosquitto bridge speaks MQTT or MQTT-over-TLS and cannot use
|
|
842
|
+
WebSockets, so the existing `wss://` path that browsers and servers use cannot carry this. It needs
|
|
843
|
+
a TLS listener of its own on 8883, and the sensible certificate to give it is the one the web
|
|
844
|
+
server already uses for the same hostname — one certificate and one renewal, rather than two.
|
|
845
|
+
|
|
846
|
+
Find where that certificate actually is, rather than assuming: certbot's own layout under
|
|
847
|
+
`/etc/letsencrypt/live/` is only one of several, and a wildcard certificate covering more than one
|
|
848
|
+
domain is often installed by hand somewhere else entirely.
|
|
849
|
+
|
|
850
|
+
```
|
|
851
|
+
sudo grep -rn 'SSLCertificateFile\|SSLCertificateKeyFile' /etc/apache2/ # Apache
|
|
852
|
+
sudo nginx -T 2>/dev/null | grep ssl_certificate # nginx
|
|
853
|
+
```
|
|
854
|
+
|
|
855
|
+
Take the paths for the vhost serving this server's hostname — `sudo apache2ctl -S` says which
|
|
856
|
+
vhost that is — and add to production's Mosquitto configuration:
|
|
857
|
+
|
|
858
|
+
```
|
|
859
|
+
listener 8883
|
|
860
|
+
protocol mqtt
|
|
861
|
+
certfile <the SSLCertificateFile path>
|
|
862
|
+
keyfile <the SSLCertificateKeyFile path>
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
then open 8883 on the firewall.
|
|
866
|
+
|
|
867
|
+
> **`certfile` must contain the intermediate certificate as well as the server's own**, or bridges
|
|
868
|
+
> will refuse to connect even though browsers are happy — a browser can often fill in a missing
|
|
869
|
+
> intermediate from cache and a bridge never can. `sudo grep -c 'BEGIN CERTIFICATE' <certfile>`
|
|
870
|
+
> should report 2 or more. If it reports 1, concatenate the issuer's chain onto a copy and point
|
|
871
|
+
> `certfile` at that.
|
|
872
|
+
|
|
873
|
+
> **The key's permissions are what usually goes wrong.** Unlike a web server, which reads its key
|
|
874
|
+
> as root at startup, Mosquitto drops to the `mosquitto` user *first* and reads the key afterwards,
|
|
875
|
+
> so a key left `root:root 0600` stops the broker starting:
|
|
876
|
+
>
|
|
877
|
+
> ```
|
|
878
|
+
> Error: Unable to load server key file "...". Check keyfile.
|
|
879
|
+
> OpenSSL Error[0]: error:8000000D:system library::Permission denied
|
|
880
|
+
> ```
|
|
881
|
+
>
|
|
882
|
+
> On Debian the tidy fix is the `ssl-cert` group, which exists for exactly this and which
|
|
883
|
+
> `/etc/ssl/private` is already set up to admit:
|
|
884
|
+
>
|
|
885
|
+
> ```
|
|
886
|
+
> sudo adduser mosquitto ssl-cert
|
|
887
|
+
> sudo chgrp ssl-cert <keyfile>
|
|
888
|
+
> sudo chmod 640 <keyfile>
|
|
889
|
+
> ```
|
|
890
|
+
>
|
|
891
|
+
> The web server is unaffected — root still reads the key as before. If you would rather not widen
|
|
892
|
+
> access to a directory holding keys for other domains, copy the certificate and key into
|
|
893
|
+
> `/etc/mosquitto/certs` owned `mosquitto:mosquitto` instead, and refresh that copy on renewal.
|
|
894
|
+
|
|
895
|
+
**Add one line to however this certificate gets renewed:**
|
|
896
|
+
|
|
897
|
+
```
|
|
898
|
+
systemctl reload mosquitto
|
|
899
|
+
```
|
|
900
|
+
|
|
901
|
+
Mosquitto keeps the certificate it loaded at startup, so without this the broker goes on serving
|
|
902
|
+
the old one until it happens to be restarted, and bridges start failing once it expires. A reload
|
|
903
|
+
is enough — it picks up a replaced certificate without dropping a single connection. Where that
|
|
904
|
+
line goes depends on how the certificate is renewed: a certbot deploy hook in
|
|
905
|
+
`/etc/letsencrypt/renewal-hooks/deploy/`, `acme.sh`'s `--reloadcmd`, or, if the certificate is
|
|
906
|
+
installed by hand, the written procedure for doing that.
|
|
907
|
+
|
|
908
|
+
> Note the asymmetry with the Pi end below: a *certificate* change needs only `reload`, but adding
|
|
909
|
+
> or changing a *bridge* needs a full `restart`, because Mosquitto does not reload bridges.
|
|
910
|
+
|
|
911
|
+
**Turn on access control.** Without it, any account that can log in to the broker can publish and
|
|
912
|
+
subscribe anywhere on it, so a per-Pi account would be no more confined than the organization's
|
|
913
|
+
own. This is the part to plan carefully, because Mosquitto's ACL file is **deny-by-default**: the
|
|
914
|
+
moment the broker names an `acl_file`, every existing account with no entry in it stops working.
|
|
915
|
+
So the file that restricts new bridge accounts has to grant the existing organizations what they
|
|
916
|
+
already have, in the same change.
|
|
917
|
+
|
|
918
|
+
```
|
|
919
|
+
sudo tee /etc/mosquitto/aclfile >/dev/null <<'EOF'
|
|
920
|
+
# Deny-by-default: an account with no rule here can connect but reach no topic at all.
|
|
921
|
+
|
|
922
|
+
# Each organization reaches only its own topic tree. "%u" is the connecting account name, and the
|
|
923
|
+
# first element of every Frugal IoT topic is the organization id - so this is what the nodes,
|
|
924
|
+
# dashboards and the server's own logger already do. It just stops being optional.
|
|
925
|
+
pattern readwrite %u/#
|
|
926
|
+
EOF
|
|
927
|
+
sudo chmod 644 /etc/mosquitto/aclfile
|
|
928
|
+
```
|
|
929
|
+
|
|
930
|
+
`chmod 644` matters: like the password file and the TLS key, the broker reads this *after* dropping
|
|
931
|
+
to the `mosquitto` user, so a root-only file stops it starting. Then name it in the configuration
|
|
932
|
+
and restart:
|
|
933
|
+
|
|
934
|
+
```
|
|
935
|
+
echo 'acl_file /etc/mosquitto/aclfile' | sudo tee /etc/mosquitto/conf.d/zy-frugal-iot-acl.conf
|
|
936
|
+
sudo systemctl restart mosquitto
|
|
937
|
+
```
|
|
938
|
+
|
|
939
|
+
Check every existing organization can still log in **before** connecting any Pi — a wrong ACL file
|
|
940
|
+
locks out every node and dashboard at once, and the reason appears only in Mosquitto's own log. On a
|
|
941
|
+
Frugal IoT server the quickest check is its journal, which logs one line per organization:
|
|
942
|
+
|
|
943
|
+
```
|
|
944
|
+
sudo journalctl -u frugaliot -n 40 --no-pager | grep -iE "mqtt |not authoris"
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
Every organization should show `connect` and none should show `not authorized`. To back the change
|
|
948
|
+
out, delete `/etc/mosquitto/conf.d/zy-frugal-iot-acl.conf` and restart.
|
|
949
|
+
|
|
950
|
+
Nothing needs adding here per Pi: `frugal-iot-addbridge-prod` in 11b appends each bridge's own rule.
|
|
951
|
+
|
|
952
|
+
> One thing this costs: `pattern readwrite %u/#` also denies `$SYS`, so the production broker can no
|
|
953
|
+
> longer tell anyone which bridges are currently connected. The Pi still reports its own bridge
|
|
954
|
+
> state to `frugal-iot-diagnostic`, which is where you would look anyway, so this is a fair trade —
|
|
955
|
+
> but it is why production's dashboard cannot show a site as up or down.
|
|
956
|
+
|
|
957
|
+
#### 11b. Authorizing this Pi (run on the production server)
|
|
958
|
+
|
|
959
|
+
Once per Pi. On the production server, from its own directory:
|
|
960
|
+
|
|
961
|
+
```
|
|
962
|
+
npx --no frugal-iot-addbridge-prod <org-id> <site-name>
|
|
963
|
+
```
|
|
964
|
+
|
|
965
|
+
The site name only distinguishes one Pi from another within an organization — `northfield`,
|
|
966
|
+
`shed`, `village2` — and becomes part of the account name. The script creates the broker account,
|
|
967
|
+
adds its access-control rule, reloads the broker, and prints both the password and the exact
|
|
968
|
+
command to run in 11c.
|
|
969
|
+
|
|
970
|
+
Three things it checks, because each of them is a way this quietly fails:
|
|
971
|
+
|
|
972
|
+
* **The organization must already exist on the production server**, or the readings will arrive at
|
|
973
|
+
its broker and nothing will record them — that server's logger subscribes per organization,
|
|
974
|
+
driven by the files in its `config.d/organizations/`. If it is missing, add it there first with
|
|
975
|
+
`frugal-iot-addorganization`, using the *same* organization id as this Pi.
|
|
976
|
+
* **Each Pi gets its own account**, rather than sharing the organization's. The organization's
|
|
977
|
+
broker password is handed to every browser that logs in, so it is not a secret; a per-Pi account
|
|
978
|
+
can be confined to one site and revoked on its own.
|
|
979
|
+
* **An existing account is left alone.** Re-running it for a site that already has one is refused
|
|
980
|
+
rather than quietly issuing a new password, which would stop that Pi relaying without anyone
|
|
981
|
+
touching it.
|
|
982
|
+
|
|
983
|
+
The password is shown once and is not stored anywhere you can read back — copy it before you lose
|
|
984
|
+
the output.
|
|
985
|
+
|
|
986
|
+
#### 11c. Bridging this Pi (run on the Pi)
|
|
987
|
+
|
|
988
|
+
Once per Pi. Run the command 11b printed, from this installation's directory:
|
|
989
|
+
|
|
990
|
+
```
|
|
991
|
+
cd ~/frugal-iot
|
|
992
|
+
npx --no frugal-iot-addbridge-pi <org-id> <prod-host> bridge-<site-name>
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
It asks for the password rather than taking it on the command line, so it stays out of your shell
|
|
996
|
+
history. Then it checks the far end is reachable and that its certificate is valid for that name,
|
|
997
|
+
writes `/etc/mosquitto/conf.d/frugal-iot-bridge.conf`, restarts the broker, and reports whether the
|
|
998
|
+
bridge actually connected.
|
|
999
|
+
|
|
1000
|
+
The restart is unavoidable: Mosquitto does not pick up bridges on a reload signal, so `reload`
|
|
1001
|
+
appears to succeed and changes nothing. It briefly disconnects every node, which they recover from
|
|
1002
|
+
on their own.
|
|
1003
|
+
|
|
1004
|
+
> The configuration goes into `/etc/mosquitto/conf.d/` and not into this installation's
|
|
1005
|
+
> `config.d/`, because it holds a password for the production server. Everything under `config.d/`
|
|
1006
|
+
> is served to any logged-in browser by `/config.json`, so a credential put there would not stay
|
|
1007
|
+
> private. The file is written mode 600 for the same reason.
|
|
1008
|
+
|
|
1009
|
+
To undo it: `sudo rm /etc/mosquitto/conf.d/frugal-iot-bridge.conf && sudo systemctl restart
|
|
1010
|
+
mosquitto`. The Pi goes back to being self-contained and keeps everything it has recorded.
|
|
1011
|
+
|
|
1012
|
+
#### Check it
|
|
1013
|
+
|
|
1014
|
+
```
|
|
1015
|
+
npx --no frugal-iot-diagnostic
|
|
1016
|
+
```
|
|
1017
|
+
|
|
1018
|
+
The **Bridge to a production server** section names the bridge, lists what it relays, and reports
|
|
1019
|
+
whether it is connected. `CONNECTED` means the link is up. `DOWN`, or a report that it has never
|
|
1020
|
+
connected, means the address, the credentials or the certificate is wrong — and Mosquitto's own log,
|
|
1021
|
+
quoted earlier in the same output, says which.
|
|
1022
|
+
|
|
1023
|
+
Then confirm at the far end: log in to production's dashboard and look for this Pi's nodes. They
|
|
1024
|
+
should appear within a couple of minutes, which is however long it takes each node to next report.
|
|
1025
|
+
|
|
1026
|
+
**One oddity worth expecting.** Each time the bridge reconnects, the most recent value of every
|
|
1027
|
+
reading is re-sent to production and recorded there with the reconnect time rather than the time
|
|
1028
|
+
it was measured. That is one row per topic per outage, and it happens because nodes publish
|
|
1029
|
+
readings retained. The `duplicates:` rules in `config.d/schema/topics.yaml` absorb it when the
|
|
1030
|
+
value has not moved and the outage was short; after a long outage the row is written.
|
|
1031
|
+
|
|
1032
|
+
**The clock matters more once a Pi is bridged**, because its readings now sit alongside
|
|
1033
|
+
production's. A bridged Pi has internet access whenever the link is up, so it sets its time from
|
|
1034
|
+
the network and the problem below mostly goes away — but it still comes up after a power cut
|
|
1035
|
+
believing whatever it last saved, and anything recorded before it reaches a time server carries
|
|
1036
|
+
that wrong time.
|
|
1037
|
+
|
|
670
1038
|
### Known limitation: the clock on an offline Pi
|
|
671
1039
|
|
|
672
1040
|
A Raspberry Pi has no battery-backed clock. While it has internet access it sets its time from the
|
|
@@ -755,6 +1123,8 @@ broken. It covers most of the checks scattered through this guide, in one pass:
|
|
|
755
1123
|
* whether Mosquitto and the `frugaliot` service are running, with the last lines of the journal
|
|
756
1124
|
**and of Mosquitto's own log**, which is where its startup errors actually appear
|
|
757
1125
|
* whether the broker refuses a wrong password, and accepts each organization's real one
|
|
1126
|
+
* whether a bridge to a production server is configured, what it relays, and whether it is
|
|
1127
|
+
connected right now (step 11) — the bridge password is not printed
|
|
758
1128
|
* whether ports 1883, 9012 and 8080 are listening, and whether the web server answers
|
|
759
1129
|
* whether the broker host in `config.d/mqtt.yaml` resolves from this machine
|
|
760
1130
|
* how much data the logger has written
|
|
@@ -770,6 +1140,34 @@ If you have not reached step 4 yet, the command does not exist. Copy
|
|
|
770
1140
|
[scripts/diagnostic.zsh](https://github.com/mitra42/frugal-iot-server/blob/main/scripts/diagnostic.zsh)
|
|
771
1141
|
to the Pi and run `zsh diagnostic.zsh` instead.
|
|
772
1142
|
|
|
1143
|
+
### A topic that will not go away
|
|
1144
|
+
|
|
1145
|
+
Nodes publish almost everything **retained**, meaning the broker keeps the last value and gives it
|
|
1146
|
+
to every new subscriber — which is how a dashboard shows a reading immediately instead of waiting
|
|
1147
|
+
for the next one. The catch is that a topic published by mistake outlives the mistake. Misspell a
|
|
1148
|
+
field, rename a module, flash a node with the wrong id, and that topic sits on the broker for ever,
|
|
1149
|
+
appearing on every dashboard. Fixing the node does not remove it, and neither does restarting the
|
|
1150
|
+
broker: the only way is to publish an empty message to that exact topic.
|
|
1151
|
+
|
|
1152
|
+
```
|
|
1153
|
+
cd ~/frugal-iot
|
|
1154
|
+
npx --no frugal-iot-clearretained 'myfarm/lotus/+/sht/temperture/#'
|
|
1155
|
+
```
|
|
1156
|
+
|
|
1157
|
+
Quote the pattern or the shell will expand it. That lists what is retained and changes nothing;
|
|
1158
|
+
add the word `delete` at the end of the same command to remove it.
|
|
1159
|
+
|
|
1160
|
+
> `delete` is a bare word rather than a `--delete` flag on purpose. npm parses the command line of
|
|
1161
|
+
> an `npx` invocation itself and swallows any `--flag` it does not recognise, so `--delete` would
|
|
1162
|
+
> never reach the script and it would quietly list instead of deleting.
|
|
1163
|
+
|
|
1164
|
+
> Look before deleting. A node's `min`, `max`, `color` and `wired` settings are retained messages
|
|
1165
|
+
> too, and they are how the dashboard knows how to draw it — delete those and the node has to be
|
|
1166
|
+
> restarted before it looks right again. A pattern of just `#` is refused outright.
|
|
1167
|
+
|
|
1168
|
+
On a Pi that is bridged to a production server (step 11), deleting here deletes there too: the
|
|
1169
|
+
bridge forwards the empty message like any other.
|
|
1170
|
+
|
|
773
1171
|
---
|
|
774
1172
|
|
|
775
1173
|
## Upgrading
|
package/README.md
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
## Installation
|
|
4
4
|
|
|
5
5
|
For a self-contained server on a Raspberry Pi - including the MQTT broker, and starting from a
|
|
6
|
-
blank SD card - see [INSTALLATION.md](INSTALLATION.md)
|
|
6
|
+
blank SD card - see [INSTALLATION.md](INSTALLATION.md), which also has a script that does the whole
|
|
7
|
+
thing in one go once you can ssh in.
|
|
7
8
|
|
|
8
9
|
On a unix box that is already running ...
|
|
9
10
|
|
|
@@ -20,15 +21,20 @@ npm install frugal-iot-server
|
|
|
20
21
|
npx --no frugal-iot-init
|
|
21
22
|
```
|
|
22
23
|
`frugal-iot-init` copies in the configuration files, creates the `data`, `ota` and
|
|
23
|
-
`config.d/organizations` directories,
|
|
24
|
-
|
|
24
|
+
`config.d/organizations` directories, points `config.d/server.yaml` at the web client npm just
|
|
25
|
+
installed, and creates the database. It never overwrites anything already there, so run it again
|
|
26
|
+
after an upgrade to pick up newly added configuration.
|
|
25
27
|
|
|
26
28
|
Edit `config.yaml` and `config.d/mqtt.yaml` (which broker to talk to) if the defaults do not suit.
|
|
27
29
|
|
|
28
|
-
|
|
30
|
+
A newly created database has two accounts in it, `everyone` (which holds the permissions every
|
|
31
|
+
logged-in user gets) and `superuser`, which has no password - so give it one:
|
|
29
32
|
```
|
|
30
33
|
npx --no frugal-iot-setpassword superuser "<a-good-password>"
|
|
31
34
|
```
|
|
35
|
+
On a database that existed before this account was introduced, that command reports
|
|
36
|
+
`no user 'superuser'`, because id 1 was already taken by whoever registered first - and that
|
|
37
|
+
account already has the superuser's privileges. Log in as that one instead.
|
|
32
38
|
|
|
33
39
|
Then add an organization - this writes its yaml file into `config.d/organizations`, creates a
|
|
34
40
|
login account of the same name with its permissions, and sets its password on the MQTT broker:
|
|
@@ -52,24 +58,49 @@ readYamlConfigFile ./config.yaml
|
|
|
52
58
|
... and then reading each of the other files
|
|
53
59
|
|
|
54
60
|
Broker wss://frugaliot.naturalinnovation.org/wss - organizations: dev
|
|
61
|
+
Not logging HTTP requests (morgan: false in config.d/server.yaml)
|
|
55
62
|
Doing OTA updates at /ota_update from ...some path.../ota
|
|
56
|
-
Serving /node_modules from ./node_modules
|
|
63
|
+
Serving /node_modules from node_modules/frugal-iot-client/node_modules then ./node_modules
|
|
57
64
|
User Database exists
|
|
58
65
|
Opened user database
|
|
59
66
|
Exec-ed starting SQL
|
|
67
|
+
Created logger client for API integration
|
|
68
|
+
Mounted API routes at /api
|
|
60
69
|
Serving /data from ./data
|
|
61
70
|
Server starting on port 8080
|
|
71
|
+
Logger not reporting individual messages (verbose: false in config.d/logger.yaml)
|
|
72
|
+
Collecting readings in memory, writing them out every 300 seconds
|
|
62
73
|
mqtt dev connecting
|
|
63
74
|
mqtt dev connect
|
|
64
75
|
Subscribing topic dev/# 0
|
|
65
|
-
Received dev/lotus/esp8266-85ea2b/humidity 71.8
|
|
66
76
|
```
|
|
67
|
-
Where the broker and organizations are reported back,
|
|
68
|
-
then it successfully connects to the mqtt server
|
|
69
|
-
|
|
77
|
+
Where the broker and organizations are reported back,
|
|
78
|
+
then it successfully connects to the mqtt server.
|
|
79
|
+
|
|
80
|
+
**It then goes quiet, and that is what should happen.** A new installation is set up to write as
|
|
81
|
+
little as it can, because the usual home for this is a Raspberry Pi running from an SD card, and
|
|
82
|
+
cards wear out from being written to. So it does not log a line per message received, nor a line
|
|
83
|
+
per web request, and readings are collected in memory and written out every five minutes rather
|
|
84
|
+
than one at a time. Seeing nothing after the lines above does not mean nothing is arriving.
|
|
85
|
+
|
|
86
|
+
To watch the messages while setting up, or to work out why a node's readings are not appearing,
|
|
87
|
+
turn it back on in `config.d/logger.yaml` and restart:
|
|
88
|
+
```
|
|
89
|
+
verbose: true
|
|
90
|
+
```
|
|
91
|
+
and each message reappears as `Received dev/lotus/esp8266-85ea2b/humidity 71.8`. `morgan: true`
|
|
92
|
+
in `config.d/server.yaml` does the same for web requests. Turn both off again afterwards.
|
|
93
|
+
There is more on what gets written, and how to see how much, under
|
|
94
|
+
[Wear and tear on the SD card](INSTALLATION.md#wear-and-tear-on-the-sd-card).
|
|
70
95
|
|
|
71
96
|
Open a browser pointing at for example `localhost:8080` and you should see the UI.
|
|
72
97
|
|
|
98
|
+
If something is not right, this reports on the whole installation - versions, configuration,
|
|
99
|
+
broker logins, what has been logged - and changes nothing:
|
|
100
|
+
```
|
|
101
|
+
npx --no frugal-iot-diagnostic
|
|
102
|
+
```
|
|
103
|
+
|
|
73
104
|
To upgrade later: `npm update frugal-iot-server` then `npx --no frugal-iot-init`.
|
|
74
105
|
|
|
75
106
|
#### Developing the server, client or logger
|
|
@@ -85,22 +116,66 @@ The commands above have in-repo equivalents - `scripts/addorganization.zsh` and
|
|
|
85
116
|
`scripts/setpassword.zsh` - and the server is `node frugal-iot-server.js`. All of them work on
|
|
86
117
|
the directory you run them in, which for a clone is the top of the repo.
|
|
87
118
|
|
|
88
|
-
To work on the client or logger from sibling checkouts,
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
119
|
+
To work on the client or the logger from sibling checkouts as well, link them in. This needs no
|
|
120
|
+
edit to any file, so there is nothing to remember to change back:
|
|
121
|
+
```
|
|
122
|
+
cd ../frugal-iot-client && npm link
|
|
123
|
+
cd ../frugal-iot-logger && npm link
|
|
124
|
+
cd ../frugal-iot-server && npm link frugal-iot-client frugal-iot-logger
|
|
125
|
+
```
|
|
126
|
+
The server then loads both from your checkouts. `ls -l node_modules/frugal-iot-*` shows whether the
|
|
127
|
+
links are in place - a later `npm install` or `npm update` can replace them with the published
|
|
128
|
+
packages, which looks like your changes having no effect.
|
|
129
|
+
|
|
130
|
+
There are commented-out alternatives in `config.d/server.yaml` (for `htmldir`) and in
|
|
131
|
+
`frugal-iot-server.js` (for the `MqttLogger` import) that do the same job by editing instead. They
|
|
132
|
+
work, but they are easy to publish by accident, so prefer the links.
|
|
92
133
|
|
|
93
|
-
|
|
94
|
-
|
|
134
|
+
Before publishing, run:
|
|
135
|
+
```
|
|
136
|
+
npm run prerelease
|
|
137
|
+
```
|
|
138
|
+
It checks the sensor schema and copies it into the examples that ship with `frugal-iot-logger`, sets
|
|
139
|
+
the service worker's cache version from the client version this release installs, and refuses if:
|
|
140
|
+
- the package is wired to a local checkout - a `file:` dependency, an `npm link`, a switched import
|
|
141
|
+
or a switched `htmldir`;
|
|
142
|
+
- it requires a version of the client or logger that has not been published (a warning if published
|
|
143
|
+
but not the newest, since a fresh install resolves to the newest anyway while an existing one may
|
|
144
|
+
not move);
|
|
145
|
+
- anything git ignores would be published. `files` in `package.json` is an allow-list, which fails
|
|
146
|
+
closed, but a file sitting in one of the listed directories goes out whether or not you meant it
|
|
147
|
+
to - which is how a working copy of the user database was published in 0.3.2.
|
|
148
|
+
|
|
149
|
+
It also lists anything in the repository that would *not* be published and is not already accounted
|
|
150
|
+
for in `scripts/not-published.txt`, so a newly added file that installers need does not go missing
|
|
151
|
+
unnoticed. `npm run check-files` runs just that part.
|
|
152
|
+
|
|
153
|
+
`npm run check-schema` on its own reports sensor topics that do not say whether they are logged, or
|
|
154
|
+
that are logged with no rule about how often - both of which are easy to add by accident and
|
|
155
|
+
expensive on an SD card.
|
|
95
156
|
|
|
96
157
|
#### Running a production server
|
|
97
158
|
To set it up as a service that runs at startup (and instructions vary between flavors of Linux)
|
|
98
159
|
|
|
99
|
-
copy and edit `extras/frugaliot.service` to `/etc/systemd/system/frugaliot.service
|
|
100
|
-
|
|
160
|
+
copy and edit `extras/frugaliot.service` to `/etc/systemd/system/frugaliot.service`. As shipped it
|
|
161
|
+
matches the Raspberry Pi install in [INSTALLATION.md](INSTALLATION.md) - user `pi`, installed in
|
|
162
|
+
`/home/pi/frugal-iot` - so on any other machine change `User`, `WorkingDirectory` and `ExecStart`
|
|
163
|
+
to the account and directory you installed into.
|
|
164
|
+
```
|
|
165
|
+
sudo cp extras/frugaliot.service /etc/systemd/system/frugaliot.service
|
|
166
|
+
sudo systemctl daemon-reload
|
|
167
|
+
sudo systemctl enable --now frugaliot
|
|
168
|
+
```
|
|
169
|
+
`daemon-reload` is the step that is easy to miss - without it systemd keeps using the version it
|
|
170
|
+
read before, and your edits appear to do nothing. After that `service frugaliot start|stop|restart`
|
|
171
|
+
works as usual, and `systemctl status frugaliot` reports whether it is running.
|
|
172
|
+
|
|
173
|
+
If you already have the service installed and only want to change one setting, `sudo systemctl edit
|
|
174
|
+
frugaliot` writes an override rather than touching the file, which is easier to undo
|
|
175
|
+
(`sudo systemctl revert frugaliot`).
|
|
101
176
|
|
|
102
|
-
|
|
103
|
-
|
|
177
|
+
Stop it with `service frugaliot stop` rather than killing it, so that readings still held in memory
|
|
178
|
+
are written out first.
|
|
104
179
|
|
|
105
180
|
Note that this will give you a HTTP server, but OTA on ESP32 requires HTTPS.
|
|
106
181
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Outgoing mail. Only used by the password reset, which is unavailable until this is filled in -
|
|
2
|
+
# the login page says so rather than pretending to have sent something.
|
|
3
|
+
#
|
|
4
|
+
# Leave it commented out on a server that should not send mail.
|
|
5
|
+
#
|
|
6
|
+
# For Gmail - the common case, written out step by step in INSTALLATION.md section 9a:
|
|
7
|
+
# 1. Turn on 2-Step Verification: https://myaccount.google.com/signinoptions/twosv
|
|
8
|
+
# App passwords do not exist until you do, which is where most people get stuck.
|
|
9
|
+
# 2. Create an app password: https://myaccount.google.com/apppasswords
|
|
10
|
+
# You get 16 lowercase letters in four groups of four, shown once. Remove the spaces.
|
|
11
|
+
# 3. "from" must be the same address as "user" - Gmail silently rewrites anything else.
|
|
12
|
+
#
|
|
13
|
+
# host: smtp.gmail.com
|
|
14
|
+
# port: 587 # 465 is implicit TLS, 587 and 25 are STARTTLS; secure: is derived from this
|
|
15
|
+
# user: yourname@gmail.com
|
|
16
|
+
# pass: "abcdefghijklmnop" # the app password, NOT your Google account password
|
|
17
|
+
# from: Frugal IoT <yourname@gmail.com>
|
|
18
|
+
#
|
|
19
|
+
# Any other provider is the same four settings with its own SMTP host.
|
|
20
|
+
#
|
|
21
|
+
# The reset email contains a link back to this server. Set baseurl if the server cannot work out
|
|
22
|
+
# its own public address from the request - behind a proxy that does not set X-Forwarded-Host, say.
|
|
23
|
+
# baseurl: https://frugaliot.example.org
|