frugal-iot-server 0.3.8 → 2.0.0

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,42 +40,6 @@ 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
- ## The short way: one script
44
-
45
- Steps 3 to 8 below can be done for you. Once you can ssh into the Pi (steps 1 and 2, which need a
46
- person with an SD card):
47
-
48
- ```
49
- curl -fsSLO https://raw.githubusercontent.com/mitra42/frugal-iot-server/main/scripts/install-pi.sh
50
- bash install-pi.sh --org myfarm --name "My Farm" --email you@example.com --phone +61123456789
51
- ```
52
-
53
- It asks for anything it needs that you did not pass, says what it is doing as it goes, and stops at
54
- the first thing that fails - leaving a log, and telling you to run `frugal-iot-diagnostic`. Running
55
- it again after a failure carries on rather than starting over, which matters on a Pi Zero W where one
56
- step takes about 40 minutes. `--dry-run` checks the arguments and shows the plan without touching the
57
- machine.
58
-
59
- **Passwords** are asked for, not invented, unless you say otherwise: it prompts for each (twice, not
60
- echoed) and generates one if you just press Enter. Pass them as `--superuser-password`,
61
- `--login-password` and `--broker-password` to skip the prompts, `--random-passwords` to have all
62
- three generated, or `--yes` to ask nothing at all — which is what to use over ssh with no terminal.
63
- A password you chose is not written to the log; a generated one has to be, since otherwise you would
64
- have no way of knowing it.
65
-
66
- **It tells you whether a reboot is needed**, rather than leaving you to wonder — it notices a kernel
67
- or boot-firmware package in the upgrade, the images in `/boot` changing underneath it, and
68
- `/var/run/reboot-required`. If none of those happened it says so, and there is nothing more to do.
69
-
70
- Fetch it with `curl -O` and then run it, rather than piping curl into bash - piped, it has no
71
- terminal to ask questions at.
72
-
73
- It finishes with a live server and a tested broker. It cannot do step 9, pointing your nodes at it,
74
- because a node learns its broker by being flashed with it - so it prints the settings to give them.
75
- Nor step 10, HTTPS.
76
-
77
- The steps below are what it does, in the same order, if you would rather do it by hand or need to
78
- understand what went wrong.
79
43
 
80
44
  ---
81
45
  ### 0. What you need before you start
@@ -163,6 +127,7 @@ From your laptop's terminal:
163
127
  ```
164
128
  ssh pi@frugaliot.local
165
129
  ```
130
+ > **Pi Zero W:** This can take a long time to connect, try `ping frugaliot.local` first to check it is alive
166
131
 
167
132
  Say `yes` to the fingerprint question, then give the password you set in Imager.
168
133
 
@@ -213,6 +178,44 @@ the password — either way `nmtui` is the fix, and it is not worth more time th
213
178
  (`sudo nmcli device wifi list` shows each nearby network's SSID and whether it is WPA2 or WPA3;
214
179
  WPA3 cannot use a derived key at all.)
215
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
+
216
219
  ### 3. Update the operating system and install the prerequisites
217
220
 
218
221
  Logged in, bring the system up to date and reboot:
@@ -500,9 +503,9 @@ npx --no frugal-iot-addorganization myfarm "My Farm" you@example.com +6112345678
500
503
 
501
504
  The arguments are: organization id, display name, your email, your phone (`+` and digits only),
502
505
  and a password. That one command writes `config.d/organizations/myfarm.yaml`, creates a login account
503
- named after the organization (`myfarm`), grants it admin rights, creates its OTA directory, **and adds
504
- the organization's account to the broker's password file** — which is why the broker had to be
505
- installed first.
506
+ named after the organization (`myfarm`), grants it admin, read and write rights, creates its OTA
507
+ directory, **and adds the organization's account to the broker's password file** — which is why the
508
+ broker had to be installed first.
506
509
 
507
510
  * **Organization id `myfarm`** — must be 1–10 lower-case letters or digits. It becomes the first part
508
511
  of every MQTT topic, so it must match what your sensor nodes are configured to publish to.
@@ -692,6 +695,101 @@ mosquitto_sub -h localhost -u myfarm -P '<broker-password>' -t '#' -v
692
695
  Every reading from every node should scroll past. Seeing anything here also proves the broker's
693
696
  port 1883 is reachable from off the Pi, which is what the nodes need.
694
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
+
695
793
  ### 10. HTTPS and over-the-air firmware updates
696
794
 
697
795
  **To be written.** Everything above gives you a plain HTTP server on your local network, which is
@@ -726,9 +824,6 @@ It comes in three parts, and only the first is a one-off:
726
824
  * **11b — authorizing this Pi on that server.** One command, run there, once per Pi.
727
825
  * **11c — pointing this Pi at it.** One command, run here, once per Pi.
728
826
 
729
- If someone has already done 11a for the server you are bridging to — the usual case once the first
730
- Pi is working — **start at 11b**.
731
-
732
827
  **What you get, and what you do not.** While the link is up, readings appear on production within
733
828
  a second or so. While it is down, the Pi records everything as usual and production simply has a
734
829
  gap — the readings taken during an outage never reach it. That is a deliberate choice: the
@@ -738,10 +833,19 @@ in the instant the link returned. A gap is honest; that would not be. Controls a
738
833
  other way round, and *are* queued, so turning something on from production while the Pi is offline
739
834
  takes effect when it reconnects.
740
835
 
836
+ **People, as well as readings.** A bridge relays topics, not accounts, so on its own it does not let
837
+ someone registered on production log in here. Step 11b also prints a *replica token*, and step 11c
838
+ asks for it; with it, this Pi pulls that organization's logins and permissions from production every
839
+ fifteen minutes, so the same people can use this Pi's dashboard — including while production is
840
+ unreachable, which is the point of the Pi. It is optional: leave the token blank and the bridge
841
+ relays readings as before. Nothing derived from a password travels either way, and each server
842
+ issues its own broker credentials, so the same person has different ones here and there. Revoking a
843
+ permission on production removes it here on the next pull. See SECURITY.md.
844
+
741
845
  #### 11a. Preparing a production server to accept bridges
742
846
 
743
- Once per production server, not per Pi — and someone has probably already done it for
744
- `frugaliot.naturalinnovation.org`. If so, skip to 11b.
847
+ Once per production server, not per Pi — and if bridging to `frugaliot.naturalinnovation.org`
848
+ this has already been done. If so, skip to 11b.
745
849
 
746
850
  **Add a listener for bridges.** A Mosquitto bridge speaks MQTT or MQTT-over-TLS and cannot use
747
851
  WebSockets, so the existing `wss://` path that browsers and servers use cannot carry this. It needs
@@ -813,12 +917,16 @@ installed by hand, the written procedure for doing that.
813
917
  > Note the asymmetry with the Pi end below: a *certificate* change needs only `reload`, but adding
814
918
  > or changing a *bridge* needs a full `restart`, because Mosquitto does not reload bridges.
815
919
 
816
- **Turn on access control.** Without it, any account that can log in to the broker can publish and
817
- subscribe anywhere on it, so a per-Pi account would be no more confined than the organization's
818
- own. This is the part to plan carefully, because Mosquitto's ACL file is **deny-by-default**: the
819
- moment the broker names an `acl_file`, every existing account with no entry in it stops working.
820
- So the file that restricts new bridge accounts has to grant the existing organizations what they
821
- already have, in the same change.
920
+ **Turn on access control** if this broker predates the release that ships it. Since
921
+ `extras/aclfile` became part of the base install, a Pi set up with `install-pi.sh` already has this
922
+ and there is nothing to do here; the steps below are for a broker built before that, or one whose
923
+ configuration was written by hand.
924
+
925
+ Without it, any account that can log in to the broker can publish and subscribe anywhere on it, so
926
+ a per-Pi account would be no more confined than the organization's own. This is the part to plan
927
+ carefully, because Mosquitto's ACL file is **deny-by-default**: the moment the broker names an
928
+ `acl_file`, every existing account with no entry in it stops working. So the file that restricts new
929
+ bridge accounts has to grant the existing organizations what they already have, in the same change.
822
930
 
823
931
  ```
824
932
  sudo tee /etc/mosquitto/aclfile >/dev/null <<'EOF'
@@ -828,12 +936,21 @@ sudo tee /etc/mosquitto/aclfile >/dev/null <<'EOF'
828
936
  # first element of every Frugal IoT topic is the organization id - so this is what the nodes,
829
937
  # dashboards and the server's own logger already do. It just stops being optional.
830
938
  pattern readwrite %u/#
939
+
940
+ # Whether a bridge is up, for frugal-iot-diagnostic. "pattern readwrite %u/#" does not match $SYS,
941
+ # so without this the diagnostic reports every bridge as never having connected. Mosquitto logs a
942
+ # warning that this pattern contains no %u - harmless, and a "topic" line would apply to anonymous
943
+ # clients only, which is not what is wanted.
944
+ pattern read $SYS/broker/connection/+/state
831
945
  EOF
832
- sudo chmod 644 /etc/mosquitto/aclfile
946
+ sudo chown mosquitto:mosquitto /etc/mosquitto/aclfile
947
+ sudo chmod 600 /etc/mosquitto/aclfile
833
948
  ```
834
949
 
835
- `chmod 644` matters: like the password file and the TLS key, the broker reads this *after* dropping
836
- to the `mosquitto` user, so a root-only file stops it starting. Then name it in the configuration
950
+ The ownership matters: like the password file and the TLS key, the broker reads this *after*
951
+ dropping to the `mosquitto` user, so a root-only file stops it starting. Mode 600 owned by
952
+ `mosquitto` rather than 644 owned by root — 2.0.21 accepts the latter but warns on every start that
953
+ a file it does not own, or one that is world readable, "will be refused by a future version". Then name it in the configuration
837
954
  and restart:
838
955
 
839
956
  ```
@@ -854,10 +971,12 @@ out, delete `/etc/mosquitto/conf.d/zy-frugal-iot-acl.conf` and restart.
854
971
 
855
972
  Nothing needs adding here per Pi: `frugal-iot-addbridge-prod` in 11b appends each bridge's own rule.
856
973
 
857
- > One thing this costs: `pattern readwrite %u/#` also denies `$SYS`, so the production broker can no
858
- > longer tell anyone which bridges are currently connected. The Pi still reports its own bridge
859
- > state to `frugal-iot-diagnostic`, which is where you would look anyway, so this is a fair trade —
860
- > but it is why production's dashboard cannot show a site as up or down.
974
+ > `pattern readwrite %u/#` on its own denies `$SYS`, which would stop `frugal-iot-diagnostic`
975
+ > reporting whether a bridge is connected it reads `$SYS/broker/connection/+/state` as the
976
+ > organization. The second rule above restores exactly that one topic and nothing else. Reading all
977
+ > of `$SYS` would also work and would hand every account the broker's client counts, subscription
978
+ > counts and traffic totals, which on a multi-organization broker tells each organization about the
979
+ > others.
861
980
 
862
981
  #### 11b. Authorizing this Pi (run on the production server)
863
982
 
@@ -894,7 +1013,7 @@ Once per Pi. Run the command 11b printed, from this installation's directory:
894
1013
 
895
1014
  ```
896
1015
  cd ~/frugal-iot
897
- npx --no frugal-iot-addbridge-pi --org <org-id> --host <prod-host> --account bridge-<site-name>
1016
+ npx --no frugal-iot-addbridge-pi <org-id> <prod-host> bridge-<site-name>
898
1017
  ```
899
1018
 
900
1019
  It asks for the password rather than taking it on the command line, so it stays out of your shell
@@ -1060,7 +1179,11 @@ npx --no frugal-iot-clearretained 'myfarm/lotus/+/sht/temperture/#'
1060
1179
  ```
1061
1180
 
1062
1181
  Quote the pattern or the shell will expand it. That lists what is retained and changes nothing;
1063
- add `--delete` to the same command to remove it.
1182
+ add the word `delete` at the end of the same command to remove it.
1183
+
1184
+ > `delete` is a bare word rather than a `--delete` flag on purpose. npm parses the command line of
1185
+ > an `npx` invocation itself and swallows any `--flag` it does not recognise, so `--delete` would
1186
+ > never reach the script and it would quietly list instead of deleting.
1064
1187
 
1065
1188
  > Look before deleting. A node's `min`, `max`, `color` and `wired` settings are retained messages
1066
1189
  > too, and they are how the dashboard knows how to draw it — delete those and the node has to be
@@ -1105,10 +1228,19 @@ command for each. That matters most for the files you copy somewhere else — a
1105
1228
  `extras/mosquitto.conf` or `extras/frugaliot.service` does nothing until you install it again:
1106
1229
 
1107
1230
  ```
1108
- sudo cp extras/mosquitto.conf /etc/mosquitto/conf.d/frugal-iot.conf && sudo systemctl restart mosquitto
1109
- sudo cp extras/frugaliot.service /etc/systemd/system/ && sudo systemctl daemon-reload && sudo systemctl restart frugaliot
1231
+ sudo cp node_modules/frugal-iot-server/extras/mosquitto.conf /etc/mosquitto/conf.d/frugal-iot.conf
1232
+ sudo cp node_modules/frugal-iot-server/extras/aclfile /etc/mosquitto/aclfile
1233
+ sudo chown mosquitto:mosquitto /etc/mosquitto/aclfile && sudo chmod 600 /etc/mosquitto/aclfile
1234
+ sudo systemctl restart mosquitto
1235
+ sudo cp node_modules/frugal-iot-server/extras/frugaliot.service /etc/systemd/system/ && sudo systemctl daemon-reload && sudo systemctl restart frugaliot
1110
1236
  ```
1111
1237
 
1238
+ Note those read from `node_modules/frugal-iot-server/extras/`, **not** from `extras/` in this
1239
+ directory. `frugal-iot-init` keeps your local copy and only tells you it differs, so the local one is
1240
+ the *old* version — copying it would reinstall what you already have. Re-running the install script
1241
+ (`bash node_modules/frugal-iot-server/scripts/install-pi.sh`) does the same job and reads from the
1242
+ same place.
1243
+
1112
1244
  > It does not compare `config.yaml`, `config.d/mqtt.yaml`, `config.d/logger.yaml` or
1113
1245
  > `config.d/server.yaml`, because those hold your own settings and would differ every time. If a
1114
1246
  > release note mentions a new setting in one of them, compare it yourself:
@@ -1124,6 +1256,32 @@ do. On a Pi, compare the two files above and copy across the settings you want.
1124
1256
  Your organizations, accounts, database and logged data are untouched by an upgrade — they live in
1125
1257
  this directory, not in `node_modules`.
1126
1258
 
1259
+ ### One thing to do by hand after upgrading: grant WRITE
1260
+
1261
+ Commanding a device through the API (`/devices/action`, `/devices/property`) now requires a **WRITE**
1262
+ permission, which is separate from READ and is *not* implied by ADMIN. New organizations get it
1263
+ automatically, but an upgrade does not touch your database, so on an existing server nobody has it
1264
+ yet and switching a relay on answers 401.
1265
+
1266
+ Grant it from the dashboard — **Admin → People**, pick the person, choose `WRITE` — or in one
1267
+ statement, to everyone who already administers an organization:
1268
+
1269
+ ```
1270
+ sqlite3 frugal-iot.db "INSERT OR IGNORE INTO permissions (id, capability, org)
1271
+ SELECT id, 'WRITE', org FROM permissions WHERE capability = 'ADMIN';"
1272
+ sudo systemctl restart frugaliot
1273
+ ```
1274
+
1275
+ Check who has it:
1276
+
1277
+ ```
1278
+ sqlite3 frugal-iot.db "SELECT id, org FROM permissions WHERE capability = 'WRITE';"
1279
+ ```
1280
+
1281
+ Do **not** grant WRITE to id `0`. That row means "every logged-in account", including anyone who has
1282
+ just registered themselves, and handing them write access is exactly what the WRITE check exists to
1283
+ prevent. (Read is a different matter — see the `(0, 'READ', ...)` rows, which are there on purpose.)
1284
+
1127
1285
  ---
1128
1286
 
1129
1287
  ## Open questions
@@ -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
@@ -59,5 +59,3 @@ soil:
59
59
  sonoff:
60
60
  # The relay is what someone wants to see; the control that drives it comes second.
61
61
  front: [relay/on, controlhysteresis]
62
- temp:
63
- front: [aht20/temperature, bmp280/pressure, controlhysteresis]