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 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, and creates the database. It never overwrites anything
24
- already there, so run it again after an upgrade to pick up newly added configuration.
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
- The database is created with a `superuser` account that has no password, so give it one:
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
- and receives data from nodes attached to it.
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, switch two places to their commented-out
89
- development lines:
90
- - `config.d/server.yaml` - `htmldir` and `nodemodulesdir` point at `../frugal-iot-client`
91
- - `frugal-iot-server.js` - the `MqttLogger` import points at `../frugal-iot-logger/index.js`
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
- Take care not to commit those local switches, and note that `npm publish` packages your working
94
- tree - so check with `npm pack --dry-run` before publishing.
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
- you'll need to change the user, the directory you installed into, and `ExecStart`
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
- You can run`service frugaliot start` to start it
103
- and `systemctl enable frugaliot` to make sure it starts at boot.
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