Skip to main content
Smarthome 17 mins

My new ESPHome multi-sensor with display for Home Assistant

With the ESPHome platform, you can implement extensive projects based on affordable ESP8266 and ESP32 controllers in a short time. In this post, I’ll show you one of my more extensive ESPHome projects.

My new ESPHome multi-sensor with display for Home Assistant
Table of Contents

My actual goal was a device based on a Wemos Mini D1 and ESPHome ESPHomeFramework for configuring ESP32/ESP8266 microcontrollers that automatically integrate into Home Assistant. Ideal for DIY sensors and actuators to read out several 1-Wire temperature sensors that I mounted on the flow and return lines of our heating system, as well as at the top, middle and bottom of our buffer storage tank. The 1-Wire sensors of type DS18B20 are not only affordable and sufficiently accurate for this task, but can also be extended well and about 100 sensors can be operated on a single data line.

In my case, however, it’s only about 9 sensors: underfloor heating flow/return, wall radiator flow/return, water-carrying fireplace flow/return and the 3 sensors in the buffer storage tank.

⚠ [affiliate] Keine Produkte mit Cache für home-assistant. Bitte affiliate-sync --lang en ausführen.

However, I always find it a shame when you use an ESP for only one task and leave the other pins unused. So why not also use a BME280 sensor right away to measure the humidity and temperature in the utility room? Thanks to the built-in air pressure sensor, you can then also have the absolute humidity and dew point calculated.

And if you already have the data, wouldn’t a display be quite nice? I also had some Nokia 5110 displays from the PCD 8544 lying around. With its 84x48 pixels, it is optimally suited for this task. With the right font, you can display 6 lines of 14 characters legibly. You simply get more on it than with a two-line HD44780 display and can also use graphics.

In addition, the ESPHome platform also supports “Pages” for this display, so you can very easily scroll through several pages and displays and thus present a lot of information clearly. However, scrolling also requires a control element and here I decided on a simple button, because there weren’t enough pins left for a rotary encoder. On the other hand, the display can be controlled in brightness via Home Assistant and a WS2812 LED strip with 8 RGB LEDs provides information about various operating states.

The build

It’s always a good idea to first build and test such more extensive projects on a breadboard before you dare to pick up the soldering iron. If the flying build works as desired, you can then tackle the implementation on a circuit board.

I’ve always been a fan of stripboard. I tinkered with it when I was 9 years old, worked with it during my apprenticeship and I prefer the stripboard philosophy to those solder rivers on perfboard. I also follow the “pure doctrine”, namely that wire bridges only run in one direction. Wemos Mini, display and BME280 sensor are plugged in so that they could also be quickly replaced.

For connecting the 9 1-Wire sensors, I decided on 9-pin spring clamp terminal blocks . 3 of them fit next to each other on a Euro board width and are ideally suited for the thin wires of the DS18B20 sensors. The data line of the sensors must be pulled to High (3.3 volt operating voltage) with a 4.7 kOhm resistor and a 10 uF electrolytic capacitor directly at the power supply of the sensors ensures more stable operation. The 1-Wire sensors are always connected in the order DATA (Yellow), VCC (Red), GND (Black) from left to right.

The complete build fit on less than half a Euro card and looks quite neat in my opinion.

Of course, you could also design and have a real circuit board made. However, I will only need this build once, so the effort is not justified here and a cleanly built stripboard is just as reliable.

Programming with ESPHome

ESPHome is a grandiose solution for your own sensors and actuators. It is based on Arduino libraries, but is mainly configured with YAML YAMLData format (YAML Ain’t Markup Language). Used in Home Assistant for configurations, automations and scenes. Easy to read but error-prone with incorrect indentation rather than programmed. Nevertheless, complex functions and calculations can also be implemented with it, by using so-called lambdas to also use C code within the YAML configuration. However, what simplifies programming even further is the integration of the ESPHome platform directly into Home Assistant. If you have the ESPHome integration in HA, you can do without flash tools, etc.

For this, you only need the Chrome browser or Edge (presumably other browsers based on Chromium also work), because these offer the Web Serial interface. With this, you connect the ESP via USB to the computer you’re currently sitting at, open the ESPHome integration in HA in the browser and can flash the ESP directly with it. The only important thing here is that the connection to Home Assistant is via SSL.

Once you’ve done that, the newly created and flashed ESP is displayed as “online” in the ESPHome interface and can be programmed directly via the web interface from then on. The logs of the ESP are also output via it and ESPs can also be updated from there. I used to use Tasmota TasmotaOpen source firmware for ESP8266/ESP32-based smart home devices (sockets, lights, relays). Replaces manufacturer firmware for local control without cloud and ESPeasy , but ESPHome is so much simpler, more elegant and more flexible to use - especially if you have several such devices in use. The ESPHome devices can be programmed from practically anywhere you have access to your Home Assistant server.

The code for the multi-sensor with display

Here is the complete program code of my ESPHome multi-sensor. The YAML configuration is largely self-explanatory. The most important sections are explained in detail below the code.

yaml
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
esphome:
  name: technik
  platform: ESP8266
  board: d1_mini

# Disable logging on RX/TX, as these pins are used as GPIOs
logger:
  baud_rate: 0

# Enable Home Assistant API
api:

ota:
  password: "Your-OTA-Password"

wifi:
  ssid: "YOUR-SSID"
  password: "YOUR-WIFI-PASSWORD"

  # Enable fallback hotspot (captive portal) in case WiFi is missing
  ap:
    ssid: "Technik Fallback Hotspot"
    password: "YOUR_PASSWORD"

captive_portal:

# GPIO overview and connections
# D0 = RST Display
# D1 = I2C SCL BME280
# D2 = I2C SDA BME280
# D3 = DC Display
# D4 = 1Wire Data for DS18B20 sensors
# D5 = SPI CLK Display
# D6 = WS2812 LED Data
# D7 = CS/CE Display
# D8 = SPI MOSI Display Din
# RX = Backlight Display = GPIO3
# TX = Switch = GPIO1
# A1 = not yet in use

light:
    # WS2812 LED info strip
  - platform: fastled_clockless
    chipset: WS2812B
    id: light_fastled
    pin: D6
    num_leds: 8
    rgb_order: GRB
    name: "Info strip"
    effects:
      - pulse:
  # Define LEDs
  - platform: partition
    name: "PL0"
    segments:
      - id: light_fastled
        from: 0
        to: 0
    effects:
      - pulse:
  - platform: partition
    name: "PL1"
    segments:
      - id: light_fastled
        from: 1
        to: 1
    effects:
      - pulse:
  - platform: partition
    name: "PL2"
    segments:
      - id: light_fastled
        from: 2
        to: 2
    effects:
      - pulse:
  - platform: partition
    name: "PL3"
    segments:
      - id: light_fastled
        from: 3
        to: 3
    effects:
      - pulse:
  - platform: partition
    name: "PL4"
    segments:
      - id: light_fastled
        from: 4
        to: 4
    effects:
      - pulse:
  - platform: partition
    name: "PL5"
    segments:
      - id: light_fastled
        from: 5
        to: 5
    effects:
      - pulse:
  - platform: partition
    name: "PL6"
    segments:
      - id: light_fastled
        from: 6
        to: 6
    effects:
      - pulse:
  - platform: partition
    name: "PL7"
    segments:
      - id: light_fastled
        from: 7
        to: 7
    effects:
      - pulse:

# Display backlight
  - platform: monochromatic
    name: "Technik Display"
    output: technik_display

output:
  - platform: esp8266_pwm
    id: technik_display
    pin: GPIO03
    inverted: true

# 1-Wire sensors
dallas:
  - pin: D4

# I2C bus for BME280
i2c:
  sda: D2
  scl: D1
  scan: true
  id: bus_a

# SPI bus for display
spi:
  clk_pin: D5
  mosi_pin: D8

# Get status data for the ESP
text_sensor:
  - platform: template
    name: Uptime Human Readable
    id: uptime_human
    icon: mdi:clock-start

  - platform: wifi_info
    ip_address:
      name: ESP IP Address
      id: my_ip
    ssid:
      name: ESP Connected SSID
      id: my_SSID
    bssid:
      name: ESP Connected BSSID
    mac_address:
      name: ESP Mac Wifi Address
      id: my_mac

# Integrate 1-Wire sensors
sensor:
  - platform: dallas
    address: 0x42030297792CC028
    resolution: 12
    name: "Flow UFH"
    id: fbh_vl

  - platform: dallas
    address: 0x16030297794F1A28
    resolution: 12
    name: "Return UFH"
    id: fbh_rl

# BME280 sensor
  - platform: bme280
    temperature:
      name: "ESP1 Technik Temperature"
      id: bme280_temperature
      oversampling: 16x
    pressure:
      name: "ESP1 Technik Air Pressure"
      id: bme280_pressure
    humidity:
      name: "ESP1 Technik Humidity"
      id: bme280_humidity
    address: 0x76
    update_interval: 120s # longer update intervals prevent sensor heating
    iir_filter: 4x # smooth measurements

# Get WiFi signal strength
  - platform: wifi_signal
    name: "Technik ESP Wifi Signal"
    update_interval: 30s
    id: wlan_signal

# Read ESP uptime and format human-readable
  - platform: uptime
    name: Uptime ESP Technik
    id: uptime_sensor
    update_interval: 60s
    on_raw_value:
      then:
        - text_sensor.template.publish:
            id: uptime_human
            state: !lambda |-
              int seconds = round(id(uptime_sensor).raw_state);
              int days = seconds / (24 * 3600);
              seconds = seconds % (24 * 3600);
              int hours = seconds / 3600;
              seconds = seconds % 3600;
              int minutes = seconds /  60;
              seconds = seconds % 60;
              return (
                (days ? String(days) + "d " : "") +
                (hours ? String(hours) + "h " : "") +
                (minutes ? String(minutes) + "m " : "") +
                (String(seconds) + "s")
              ).c_str();

# Get LCN-WIH outside temperature sensor from HA
  - platform: homeassistant
    id: temp_aussen
    entity_id: sensor.wih_temp_aussen

# Calculate air pressure with altitude correction to 516 meters

  - platform: template
    name: "Technik Air Pressure"
    update_interval: 60s
    lambda: |-
      const float STANDARD_ALTITUDE = 516; // Height above sea level at location in meters
      return id(bme280_pressure).state / powf(1 - ((0.0065 * STANDARD_ALTITUDE) /
        (id(bme280_temperature).state + (0.0065 * STANDARD_ALTITUDE) + 273.15)), 5.257); // in hPa
    unit_of_measurement: 'hPa'

# Calculate absolute humidity in g/m3
  - platform: template
    name: "Technik Absolute Humidity"
    lambda: |-
      const float mw = 18.01528;    // molar mass of water g/mol
      const float r = 8.31447215;   // universal gas constant J/mol/K
      return (6.112 * powf(2.718281828, (17.67 * id(bme280_temperature).state) /
        (id(bme280_temperature).state + 243.5)) * id(bme280_humidity).state * mw) /
        ((273.15 + id(bme280_temperature).state) * r); // in grams/m^3
    accuracy_decimals: 2
    update_interval: 60s
    icon: 'mdi:water'
    unit_of_measurement: 'g/m³'
    id: wsabs

# Calculate dew point
  - platform: template
    name: "Technik Dew Point"
    lambda:
      return (243.5*(log(id(bme280_humidity).state/100)+((17.67*id(bme280_temperature).state)/
      (243.5+id(bme280_temperature).state)))/(17.67-log(id(bme280_humidity).state/100)-
      ((17.67*id(bme280_temperature).state)/(243.5+id(bme280_temperature).state))));
    unit_of_measurement: °C
    icon: 'mdi:thermometer-alert'

# Get time from Home Assistant (for uptime calculation)
time:
  - platform: homeassistant
    id: homeassistant_time

# Define display fonts
font:
  - file: "fonts/hd44780.ttf"
    id: font_a
    size: 8
  - file: "fonts/VCR_OSD_MONO.ttf"
    id: font_b
    size: 20

# Define icons
image:
  - file: "icons/water-percent.gif"
    id: water_percent
  - file: "icons/thermometer.gif"
    id: thermometer
  - file: "icons/sm_sad.gif"
    id: sad
    resize: 22x24
  - file: "icons/sm_neutral.gif"
    id: neutral
    resize: 22x24
  - file: "icons/sm_happy.gif"
    id: happy
    resize: 22x24

# Nokia 5110 Display
display:
  - platform: pcd8544
    id: my_display
    reset_pin: D0
    cs_pin: D7
    dc_pin: D3
    contrast: 0x3f # Important: too high values show black display!

    # Multiple display pages
    pages:
      - id: page1
        lambda: |-
          it.image(0, 0, id(thermometer));
          it.printf(14, 0, id(font_b), "%.1f°C", id(bme280_temperature).state);
          it.image(0, 20, id(water_percent));
          it.printf(14, 20, id(font_b), "%.0f", id(bme280_humidity).state);
          it.printf(38, 22, id(font_a), "%.1f", id(wsabs).state);
          it.print(38, 30, id(font_a), "g/m3");
          if ((id(bme280_humidity).state <= 55)) {
            it.image(63, 18, id(happy));
          }

          if ((id(bme280_humidity).state >= 56) and (id(bme280_humidity).state <= 68))    {
            it.image(63, 18, id(neutral));
          }
          if ((id(bme280_humidity).state >= 69))    {
            it.image(63, 18, id(sad));
          }

          it.strftime(0, 40, id(font_a), "%H:%M-%d.%m.%y", id(homeassistant_time).now());

      - id: page2
        lambda: |-
          it.print(0, 0, id(font_a), "   HC1 - UFH");
          it.print(0, 6, id(font_a), "______________");
          it.printf(0, 18, id(font_a), "UFH FL:%.1f C", id(fbh_vl).state);
          it.printf(0, 28, id(font_a), "UFH RL:%.1f C", id(fbh_rl).state);
          it.printf(0, 38, id(font_a), "Outside:%.1f C", id(temp_aussen).state);

      - id: page3
        lambda: |-
          it.print(0, 0, id(font_a), "  HC2 - Wall");
          it.print(0, 6, id(font_a), "______________");
          it.printf(0, 18, id(font_a), "HC FL:%.1f C", id(fbh_vl).state);
          it.printf(0, 28, id(font_a), "HC RL:%.1f C", id(fbh_rl).state);
          it.printf(0, 38, id(font_a), "Outside:%.1f C", id(temp_aussen).state);
      - id: page4
        lambda: |-
          it.print(0, 0, id(font_a), " HC3 - Fireplace");
          it.print(0, 6, id(font_a), "______________");
          it.printf(0, 18, id(font_a), "FP FL:%.1f C", id(fbh_vl).state);
          it.printf(0, 28, id(font_a), "FP RL:%.1f C", id(fbh_rl).state);
          it.printf(0, 38, id(font_a), "Outside:%.1f C", id(temp_aussen).state);
      - id: page5
        lambda: |-
          it.print(0, 0, id(font_a), "    Buffer");
          it.print(0, 6, id(font_a), "______________");
          it.printf(0, 18, id(font_a), "Top:   %.1f C", id(fbh_vl).state);
          it.printf(0, 28, id(font_a), "Middle: %.1f C", id(fbh_rl).state);
          it.printf(0, 38, id(font_a), "Bottom: %.1f C", id(fbh_rl).state);

      - id: page6
        lambda: |-
          it.print(0, 0, id(font_a), "Name: Technik");
          it.print(0, 6, id(font_a), "______________");
          it.printf(0, 16, id(font_a), "Up:%s", id(uptime_human).state.c_str());
          it.printf(0, 24, id(font_a), "%s", id(my_ip).state.c_str());
          it.printf(0, 32, id(font_a), "%s", id(my_mac).state.c_str());
          it.printf(0, 40, id(font_a), "WLAN: %.0f dBm", id(wlan_signal).state);

# Button for switching display pages
binary_sensor:
  - platform: gpio
    pin:
      number: GPIO01
      mode: INPUT_PULLUP # enable internal pullup - saves a resistor :-)
      inverted: True
    name: "Technik Button"
    on_press:
      then:
        - display.page.show_next: my_display
        - component.update: my_display

# Report ESP status to HA (connected or not)
  - platform: status
    name: "ESP Technik"

Since I don’t need the serial interface of the ESP, I disable logging for it in line 8. This allows me to use the two pins as normal GPIOs.

The most important configurations and functions in detail

Much in the code are standard functions that you can simply look up. I’d like to briefly explain the more interesting parts individually here.

The WS2812 LED strip with 8 RGB LEDs is set up with the fastled_clockless platform. For each LED, the partition platform is created so each LED can be addressed individually later. For example, you could use it to indicate when you should ventilate or when the heating burner is active.

The display backlight is connected to the former RX pin (GPIO03) and can be controlled and dimmed from HA via PWM.

The DS18D20 1-Wire temperature sensors are defined with their unique IDs. Either you connect each sensor individually one at a time, note down the ID and mark it on the sensor, or you connect all sensors at the same time, briefly warm up each sensor and check in the log which ID is currently showing a higher temperature.

The ESP uptime is converted into a human-readable format (e.g. “2d 5h 30m 15s”) using a lambda function, so it can be nicely displayed.

Sensors from Home Assistant can also be read - here I get the outside temperature from my LCN LCNLCN (Local Control Network) – German bus system for building automation by Issendorff. Uses existing 230V wiring as bus line. Can be integrated into Home Assistant via LCN-VISU or custom integration -WIH weather station.

With lambdas and C code, the absolute humidity, altitude-corrected air pressure and dew point are calculated from the BME280 values. The formulas come from the internet. For the air pressure, make sure to enter the correct altitude - the BME280 outputs uncompensated values.

The Nokia 5110 display - fonts, icons and display

Fonts must be defined for the Nokia 5110 PCD8544 display. Any TrueType font (.ttf) can be used, but for the smallest legible display, 7x5 pixel fonts are ideal. I use the hd44780.ttf for small text and VCR_OSD_MONO.ttf for large values. The fonts are loaded into a directory below the esphome directory via the VSCode integration in HA.

Icons (thermometer, water drop, 3 smileys for humidity) are defined as GIF files. I only had success with GIFs - PNGs always showed a black block.

The display is configured with the pcd8544 platform. The contrast setting is critical: the official documentation specifies 0x7f, but this results in a completely black display. With 0x3f, the font is displayed ideally.

With pages, content is distributed across multiple display pages. Page1 is the main screen showing room temperature and humidity with icons, absolute humidity, a smiley based on humidity level, and the current time/date. Pages 2-5 show the heating circuit temperatures, page 6 shows network information.

A button on GPIO01 with internal pullup switches through the display pages.

Tips

There are many discussions about the BME280 sensor online. The sensor was originally developed by Bosch . According to the Bosch datasheet , the sensors must be soldered in a special process and then dried and burned in under defined conditions.

With the cheap Chinese sensors, you can’t really be sure whether they are Bosch types or copies, and the processing process is certainly not adhered to. That’s why it’s always said that these Chinese variants are inaccurate. However, if you make sure that the sensor is mounted as far away from heat sources like the ESP8266 as possible and the leads are as long (and thin) as possible, the BME280 delivers very accurate values according to my experience and also comparison measurements with saturated salt solutions (see also: /guenstige-xiaomi-mijia-ble-sensoren-mit-home-assistant-nutzen/ ).

Often the expectation of a measurement is simply nonsense. It’s not about getting the best and most accurate measurement, but the measurement that fits the application. The air temperature differs even in small rooms and depending on the measurement height and position in the room, not infrequently by 2 °C. The same applies to humidity. So is 22.7 °C more correct than 23.4 °C? No! Because if you omit the decimal place, a completely sufficient 23 degrees would come out as the measurement for this application.

Yet even here, the measurement says nothing about whether you feel comfortable at this temperature. The comfort climate is defined by temperature, humidity and air movement. The perceived temperature increases with rising humidity and vice versa. 21 °C air temperature feels like 23 °C at 55% rel. humidity and like 26 °C at 75%. That’s why quite warm but dry heating air in winter doesn’t feel as muggy as one would often expect. If a slight air movement is also added, 23 °C can feel like 20 °C.

You should hang all DS18B20 sensors in a bucket of water whose temperature has been measured with a trustworthy thermometer. This allows you to select the sensors by accuracy and deviation. When measuring the flow and return of a low-temperature heating system, 1 °C deviation already makes a difference. If the return temperature is then higher than the flow, you can’t build a sensible control on it. Therefore, both sensors should deliver the same relative measurements. Deviations from the actual measurement can then possibly also be compensated via software.

Components and costs

The entire project costs less than 10 euros. The 5110 displays are available for less than 2 euros from various Chinese suppliers (pay attention to the new regulations for shipping from China - see HERE !). An ESP Wemos Mini also only costs 2 euros. If you order it from Germany, you have to reckon with about 4.50 euros. BME280 sensor , circuit board and the terminals are available for another 3-4 euros.

Not included in the costs are the DS18B20 1-Wire sensors. You can get 10 pieces for under 20 euros . Based on this basic code, you can cheaply produce many universal smart home sensors with a display. Since you have both an I2C and an SPI bus available, you can easily integrate additional sensors and thus expand the range of functions according to your own wishes and requirements.


Hi! I'm Markus – the Fricklr. ⚡ Electrical engineer, 🎵 musician (bass, guitar, keys) and professionally 'something with the internet' 🌐 for over 30 years – nowadays with AI too 🤖 – a 55-year-old generalist from Bavaria, Germany.

Your feedback helps me write even more interesting posts. After "Yes" there's also a small option to support my work.

Explore related topics

Content Map →
Show related content as a list
💡 Drag, hover & click to explore
Bigger dots are more relevant

What do you think? Leave a comment!

Share your thoughts, questions or experiences with the community.

Links marked with this symbol are affiliate links. As an Amazon Associate I earn from qualifying purchases. There are no additional costs for you.