How I Built a Thermostat for NuHeat Floor

NuHeat floor thermostats are great but have a couple of drawbacks: they cost a lot and they fail from electricity spikes. After two replacements, I decided that a cost of a NuHeat thermostat is sufficient budget for making something cool while not paying NuHeat for their extortive designs.


It’s a Raspberry PI-based GFCI-protected 16A thermostat with 7 inch dimmable touch screen. User interface is web-based, accessible over WiFi or the touchscreen.

User Interface

Web page running on Chromium browser in kiosk mode on a 7-inch touchscreen:


bathroom_ 2bathroom_3




Mobile User Interface

Same web page, on a mobile phone:



Bill of materials

Thermostat Base: (~$44.21) – I could’ve used just this, with user interface accessed using my mobile phone.

Screen: ($98) – Nice addition with touch control and weather display

Dimmer: ($6.95) – without it, the screen is bright enough to fully light the bathroom at night

Total: ~ $149.16 plus shipping. Pretty close.


I had to chisel back of the frame to widen it a bit to fit the screen. The components are attached to the back of the screen with small patches of 3M VHB double-sided sticky foam tape.


All connections are from Raspberry PI GPIO pins to sensor pads named correspondingly:

Light Sensor

  • 1 – +3.3V
  • 3 – I2C SDA
  • 5 – I2C SCL
  • 6 – GND

Dimmer PWM control to HDMI Driver

  • 12 – PWM (HDMI Driver backlight control pad)
  • 6 – GND (hm, looks like 14 would have been a better choice)


  • USB and HDMI connections

Note: USB cable, both ends being micro-B plugs, requires a jumper between pins 4 & 5 to indicate master device on Pi side

Also, I soldered all the brightness jumpers on HDMI driver board for maximum brightness.

Floor Sensor ADC


Relay Control


Power Supply and GFCI Outlet


relay-other side

Power supply is removed from its original bulky case and placed into a custom housing made by two diagonally cut pieces of a small plastic box.

Both relay and power supply are connected to the output terminal of the GFCI outlet – terminals protected by ground fault interruptor (the other pair of terminals are AC line inputs). Couple of nice benefits of connecting power supply after the interruptor: Raspberry can be reset by outlet’s “Reset” button or turned off altogether by the “Test” button.

Again, both power supply and relay are attached to the outlet with 3M VHB tape.


Most appreciated feature turned out to be the current weather display and weather forecast from National Weather Service.

Base Thermostat

holdtemp.py – python script reading temperature from ADC, then turning relay on or off based on the temperature threshold in /run/lock/set-temp.txt. Writes current temp to /run/lock/current-temp.txt

restart-holdtemp.sh – shell script to kill holdtemp if it hangs (and does not update current-temp.txt for 2 minutes)

settemp.sh – shell script that sets set-temp.txt – runs from cron. Ignores the setting if the file /run/lock/schedule-status.txt is not present, this is used to disable scheduled operation while leaving schedule settings in place

Web-based Controls

/var/www/thermostat – contains php scripts for reading and writing crontable and current-temp, as well as html, css, fonts and javascript to display web ui. The UI can be used from any browser, e.g. from a phone


runkiosk.sh – script to launch X11 GUI, with Chromium browser in kiosk mode pointing to the web-based thermostat user interface URL (http://localhost)


dimmer.py – script that reads light sensor and sets PWM pulse width that adjusts screen backlight

Requires pigpiod to be installed and running

Setting Up

Web server config

apt install nginx php-fpm
sudo mkdir /var/www/cache; sudo chown www-data.www-data /var/www/cache
sudo mkdir /var/www/thermostat; sudo chown www-data.www-data /var/www/thermostat

unpacked website folder from github into /var/www/thermostat and updated dependencies using npm and bower (actually, just unpacked a zip file)

added nginx config file:

sudo vi /etc/nginx/sites-enabled/bathroom

with contents

proxy_cache_path /var/www/cache levels=1:2 keys_zone=my_cache:1m max_size=1g inactive=60m use_temp_path=off;

server {

    listen 80 default_server;
    listen [::]:80 default_server;

    root /var/www/thermostat/app;
    index index.php index.html index.htm;
    server_name bathroom;
    location / {
        try_files $uri $uri/ =404;
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php7.0-fpm.sock;
    location /weather_current {
        proxy_cache my_cache;
        proxy_pass https://api.weather.gov/stations/KSFO/observations?limit=1;
        proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504;
        proxy_ignore_headers Cache-Control;
        proxy_cache_valid any 60m;
        add_header X-Cache-Status $upstream_cache_status;
    location /weather_forecast {
        proxy_cache my_cache;
        proxy_pass https://api.weather.gov/gridpoints/MTR/91,115/forecast;
        proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504;
        proxy_ignore_headers Cache-Control;
        proxy_cache_valid any 60m;
        add_header X-Cache-Status $upstream_cache_status;

The two cached URLs above are location specific, if I lived not in SF Bay Area, I would have used URLs with other coordinates in parameters.

Set webserver to auto-start:

sudo systemctl enable nginx

It will need to be started in rc.local ac described below, though, as it fails to resolve weather service URLs above on start 🙁

Screen config

edited config.txt

sudo vi /boot/config.txt

added to force 800×480 mode and to set overscan margins where screen is hidden behind frame edges:


hdmi_cvt 800 480 60 6 0 0 0

I installed package for running chromium in a kiosk mode:

apt install matchbox-window-manager nginx php-fpm

Enabled pigpio daemon:

sudo systemctl enable pigpio

edited crontab:

crontab -e

added temperature and relay control restart script

*/5 * * * * /var/www/thermostat/scripts/holdtemp_restart.sh

edited rc.local:

sudo vi rc.local

added (needed to make manual start of nginx because is needs resolver to be connected to the internet and working properly before it can start with proxy directives in the configuration):

sudo chmod 0660 /dev/tty*
sudo service nginx start
sudo -u pi xinit /var/www/thermostat/scripts/runkiosk.sh& 
sudo -u pi /var/www/thermostat/scripts/dimmer.py& 


Why ADC is read in differential mode with one input hanging in the air? Why ACD is read multiple times over several seconds and averaged?

It turned out that the thermistor cable picks up all sorts of noise, most of it being AC current. Having one ADC input in the air helps to alleviate this a bit, the rest is handled by averaging: when several full wavelengths of samples are captured, the phase of start and finish matters less.

Looking back any pitfalls I could have avoided?

Light sensor should have been placed on top of the frame. At the bottom it’s covered by hand when using touch screen, which then promptly dims.

Leave a Reply

Your email address will not be published. Required fields are marked *