Creating a smart thermostat using ESP32+openHAB+Mosquitto+Apache+letsencrypt

I wanted a smart thermostat for my village home, so that we can turn off the heating when we leave and turn it on a few hours before we’re due to arrive. Unfortunately this came with a lot of restrictions, which basically excluded almost all choices in the market currently:

  1. Encryption
  2. No admin/admin vulnerabillities
  3. Something that respects the GDPR
  4. Something ideally open source, or at least that respects the GPL
  5. Open protocol, so I don’t need to pollute my phone with yet another fishy app
  6. Something that doesn’t depend on third party servers, otherwise I risk ending up with an expensive paperweight at a random company’s whim

The excellent folks at https://hestiapi.com/ have a product that look like it’s checking all my boxes. Plus, it’s apparently a company from Athens! However, I eventually decided on a DIY solution based on one of my pre-existing servers. I’d install OpenHAB and MQTT on my server, and have an ESP32 on-site as the controller. The advantage of using OpenHAB and MQTT on a pre-existing server, as opposed to a RaspberryPi on-site, is that I don’t need to try to speak with a real person on my ISP’s tech support in order to convince them to give me a real IP address.

This blog post will cover the installation of openHAB and MQTT on an existing Apache web server using letsencrypt.

For the following instructions, assume a root shell on the server.

First of all, I installed mosquitto on my Debian server:

apt install mosquitto mosquitto-clients

Then I edited /etc/mosquitto/mosquitto.conf to make it work with a username/password and also my existing letsencrypt certificates, by adding these lines at the bottom:

tls_version tlsv1.2
listener 8883
allow_anonymous false
password_file /etc/mosquitto/users
certfile /etc/mosquitto/certs/fullchain.pem
keyfile /etc/mosquitto/certs/privkey.pem
cafile /etc/ssl/certs/DST_Root_CA_X3.pem

Now it’s referencing some files that don’t exist yet. First of all, we need to remove the existing /etc/mosquitto/certs directory and symlink it to our /etc/letsencrypt/live/example.com directory. We also need to give the mosquitto user access to the certificates by adding it to the ssl-cert group. Feel free to ignore the README that says that the directory must be readable only by the mosquitto user – having it part of the ssl-cert group works just fine.

We also need to create the /etc/mosquitto/users file. We initially edit it by adding a list of usernames and passwords, one username per line, with a colon between usernames and passwords. Example:

jimmy:password
admin:letmein

We then encrypt the file using this command:

mosquitto_passwd -U /etc/mosquitto/users

Restart the mosquitto service:

/etc/init.d/mosquitto restart

And this part is ready. Next, we install openHAB. I installed the testing distribution:

wget -qO - 'https://openhab.jfrog.io/artifactory/api/gpg/key/public' | apt-key add -
echo 'deb https://openhab.jfrog.io/artifactory/openhab-linuxpkg testing main' | tee /etc/apt/sources.list.d/openhab.list
apt update
apt install openhab openhab-addons openjdk-11-jre

Now, openHAB runs its own web server on port 8080, and 8443 for SSL using self-signed certificates. We do not want to expose port 8080 to the public. Also, for SSL it’s using certificates in a different format than letsencrypt’s default, so we would theoretically need to convert the certificates every two months and restart the openHAB server. It’s easier to configure Apache to do a reverse proxy on a different port than the default 443, which we use for our own stuff. The example that I had found online uses port 444 instead, but Firefox complains that this address is restricted. So let’s use port 1443 instead:

 <VirtualHost *:1443>
        ServerName example.com
        SSLEngine on
        SSLCertificateFile /etc/letsencrypt/live/example.com/fullchain.pem
        SSLCertificateKeyFile /etc/letsencrypt/live/example.com/privkey.pem
        Header set Set-Cookie "X-OPENHAB-AUTH-HEADER=1"
        ProxyPreserveHost On
        ProxyPass / http://127.0.0.1:8080/
        ProxyPassReverse http://127.0.0.1:8080/ /
        RequestHeader set X-Forwarded-Proto "https" env=HTTPS
        Header add Authorization ""
        RequestHeader unset Authorization
        ErrorLog ${APACHE_LOG_DIR}/openhab_error.log
        CustomLog ${APACHE_LOG_DIR}/openhab_access.log combined
        <Location />
                AuthType Basic
                AuthName "example.com 1443 "
                AuthUserFile /etc/openhab/.passwd
                Require valid-user
        </Location>
</VirtualHost>

Add the necessary NameVirtualHost *:1443 and Listen 1443 to /etc/apache2/ports.conf and you’re ready.

You will also notice that we’re password-protecting the webpage. We’ll explain the reason in a while. For now, just create the file in question:

htpasswd -c /etc/openhab/.passwd jimmy

and enter the password in the prompt.

After this is done, restart Apache, point your browser towards https://example.com:1443 and create the administrator’s username and password. You will also be prompted to install the MQTT module.

After logging in as administrator, go to Settings -> (under System Services) API Security, click “Show advanced”, and enable “Allow Basic Authentication” in order for the Android app to work. (I’m not 100% sure that this step is necessary, in fact)

Note: DO NOT disable “Implicit User Role”, as the Android app will break. It does ask for a username and password, but I think those are used for Apache’s authentication instead. I had initially tried to disable Apache’s authentication and also disable “Implicit User Role”, thinking that already gives me proper access control. The Android app failed spectacularly.

Now, let’s add a dummy thermostat. Go to Settings -> Things and click the Plus button to create a new Thing. From MQTT Binding, select MQTT Broker. Add your example.com hostname (ideally not 127.0.0.1 otherwise certificate verification will fail), port 8883 even though it’s the default, provide the username and password you configured for mosquitto, and enable Secure Connection. Your broker should show up as Online. In order to prevent it from breaking at every letsencrypt update, disable Certificate Pinning and Public Key Pinning, and clear their hashes.

For now, let’s add a dummy On/Off switch. Go back to Things and add a new Generic MQTT Thing. Give it a name, select the MQTT Broker you added earlier, and then go to Channels. Add a Channel of On/Off Switch type. Give it a name and select an MQTT State Topic, for instance thermostat/status. Leave the Command Topic empty for now, it can be a read-only switch. Its Custom On value can be 1 and its Custom Off value can be 0. It should also show up as Online.

Go back to Settings and click Items. Add an item for the switch you just added and select its channel. Let’s send a dummy command to turn it on:

mosquitto_pub --insecure -u jimmy -d -h example.com -p 8883 -t thermostat/status -m 1 -P password

It should show up as ON on the openHAB GUI. Change 1 to 0 to turn it off.

If you want to check if the command arrived to the Mosquitto server itself,
you can run a listener:

mosquitto_sub --insecure -u jimmy -d -h example.com -p 8883 -t thermostat/status -P password

While it’s running, it should show you any updates that it catches.

Note that I used the --insecure switch in both commands. I couldn’t get certificate verification to work here, but it doesn’t matter because it’s running on the host itself.

You can also install the openHAB Android client and configure it with the https://example.com:1443 remote server with your configured username and password. It will show an empty layout, but we haven’t configured our smart home’s layout yet. It will be explained in Part 2, together with the actual thermostat’s ESP32 implementation.