Multisensor
This is the project the channel is known for. One NodeMCU in a small printed case, sitting in a room, reporting four things Home Assistant can automate on: temperature, humidity, motion, and how bright the room is. The RGB LED on the front is a status light you can drive from an automation — a doorbell flash, a laundry-is-done colour, a red glow when the garage is still open.
It has been rebuilt several times since the original Arduino sketch. What’s documented here is the ESPHome version, which is the one to build today — it is a config file rather than a sketch, it updates over WiFi, and it needs no MQTT broker. The old sketches are still in the folder at the bottom for anyone maintaining a node they flashed years ago.
Parts
Some links below are Amazon affiliate links. As an Amazon Associate I earn from qualifying purchases — at no extra cost to you. It helps keep these guides free. Outside the US, they should send you to your own Amazon store; if one doesn't, searching the part number there will find it.
- NodeMCU v2 (ESP8266) — this
has to be the narrow v2 that
board: nodemcuv2names. The printed tray is 54 mm long on the outside; a wide LoLin/v3 board is 58 mm and simply doesn’t go in. - DHT22 / AM2302 module — buy the three-pin module rather than the bare sensor. It carries its own pull-up, which is one resistor you don’t have to add.
- Mini pyroelectric PIR module — the small one. See the note below before you order the other kind.
- Photoresistor light module, 10-pack — the divider is already on the board, so its analogue output goes straight to A0 with nothing between.
- 5 mm common-cathode RGB LED and an assorted resistor kit — you need three 220 Ω for the LED legs.
- 5 V 2.5 A micro-USB supply, or a USB block you already own plus a right-angle 1 ft micro-USB cable, which is what lets the case sit flat against a wall.
- Perfboard, headers and terminal blocks if you’re soldering, or Dupont jumpers if you’re not — the female-to-female ones go from the sensor modules straight onto the NodeMCU’s header, no iron involved.
- PLA+ or
PETG for the case —
nodemcu_top.stlandnodemcu_bottom.stl, both in the download section below.
The hole size picks your PIR. The bottom half has three 9.4 mm holes across it (measured off the STL — details under The case). An HC-SR501’s lens dome is far bigger than that and will not go through one, which is why the list above has the mini module. That is the part these were built with, bought by the handful alongside the DHT22s and the light modules.
If you’re putting the electronics in a box of your own, the HC-SR501 five-pack is the nicer part to live with: it has sensitivity and retrigger trimmers the mini module doesn’t.
The DHT22 is the accurate one. A DHT11 will fit the same three pins and is a few dollars cheaper, but it reads to a whole degree and drifts. If you plan to automate a thermostat off this, buy the DHT22.
The case
Two parts, and between them they tell you most of what you need to know about
the build. The tray (nodemcu_bottom.stl) is 54 × 30.5 mm and 8.3 mm deep. The
lid (nodemcu_top.stl) is the same footprint and 17 mm tall, so the assembled
sensor is about 54 × 31 mm on the wall and around 25 mm proud of it.
Every opening is in the tray. Three 9.4 mm holes sit on one centreline, 17.5 mm and 18.5 mm apart, with a 46 × 2.4 mm slot running along each long edge. That is the face the sensors look through, and those two slots are the only air path the DHT22 gets — don’t tape over them or fill them with hot glue. The lid is blank: no holes, no cutouts, nothing to line up.
Print settings
| Setting | Value |
|---|---|
| Layer height | 0.2 mm |
| Infill | 20% |
| Supports | None |
| Material | PLA or PETG |
| Orientation | Flat, as exported |
Both halves are open on one side, so printed as exported they have nothing overhanging and need no supports. PETG is the better pick if the sensor is going anywhere warm — a car, a garage, a south-facing window. PLA will sag over a summer in a hot room.
Wiring
Every pin below is the pin bruh-multisensor.yaml expects. If you wire it
differently you’ll need to change the config to match, which is a one-line edit
per sensor. The GPIO column is there because ESPHome’s logs and the ESP32 config
talk in GPIO numbers even when the silkscreen says D.
| Part | NodeMCU pin | GPIO | Notes |
|---|---|---|---|
| DHT22 data | D7 | 13 | platform: dht, model: DHT22, every 5 s |
| PIR out | D5 | 14 | binary_sensor with device_class: motion |
| Light module AO | A0 | ADC | Analogue out, not the digital threshold pin |
| RGB LED red | D1 | 5 | 220 Ω in series |
| RGB LED green | D2 | 4 | 220 Ω in series |
| RGB LED blue | D3 | 0 | 220 Ω in series — and see Troubleshooting |
| Status LED | — | 2 | The board’s own blue LED, claimed by status_led |
| Power | 3V3 and GND | — | The PIR will also run from VIN/5 V |
The RGB LED in the config is common cathode — the long leg goes to GND. If
yours is common anode the colours come out inverted, and the fix is
inverted: true on each of the three output: entries.
That onboard blue LED blinking is not a fault. status_led on GPIO2 is
deliberate: it flashes when WiFi or the API connection is down and sits dark when
everything is healthy, so you can diagnose a node from across the room.
There are Fritzing diagrams in the project folder if you’d rather work from a breadboard layout than a table.
Assembly
-
Print both halves and dry-fit the NodeMCU into the tray before you wire anything. If the board doesn’t drop in, you have the wide one.
-
Wire the sensors. Female-to-female jumpers onto the NodeMCU header is the fast route and it works; perfboard is the tidy one. Either way keep the DHT22 leads short — it is the one part where long unshielded leads cause bad readings.
-
Fit the sensors to the three holes in the tray so each one looks out through the front, and leave the two edge slots clear for the DHT22.
-
Set the PIR’s trimmers before you close the case, if yours has them. One is sensitivity, the other is how long OUT stays high after a trigger — turn the second most of the way down, because ESPHome and Home Assistant handle the “how long until we call it clear” part better than the sensor does.
-
Flash it (below), confirm the entities appear in Home Assistant, and only then put the lid on.
Flashing
-
Copy
bruh-multisensor.yamlfrom the download section below into your ESPHome folder. -
Fill in
secrets.yaml—wifi_ssid,wifi_password,ota_password,ap_password. There is asecrets.yaml.examplein the repo with every key these configs use. -
Change the two substitutions at the top so each node gets its own name:
substitutions:device_name: "Living Room Sensor"device_id: "living-room-sensor" -
Flash over USB the first time. Every flash after that is over WiFi.
It appears in Home Assistant on its own — ESPHome devices are discovered, so there is no YAML to write on the Home Assistant side.
What you get in Home Assistant
- Temperature and Humidity from the DHT22, updating every 5 seconds.
Temperature is converted to °F by a lambda filter in the config; delete that
filter block and the
unit_of_measurementline to get °C. - Motion as a
binary_sensorwithdevice_class: motion, so it drives presence automations directly. - Luminance as a percentage, calibrated with a two-point
calibrate_linearand adelta: 3filter so it isn’t reporting noise every two seconds. - A light entity for the RGB LED, with flicker, strobe and random effects.
- WiFi signal every 5 minutes, a connection status sensor, and a restart switch.
The logger is set to ERROR in the shipped config. Turn it up to DEBUG while
you’re calibrating and put it back afterwards — a chatty ESP8266 spends real time
formatting strings it could spend serving the API.
The ESP32 variant
bruh-multisensor-esp32.yaml is a different sensor stack, not just a different
board. Build it if you want accuracy; build the ESP8266 one if you want to make
six of them for the cost of two. There is no printed case for this one — the
STLs in this folder are the NodeMCU tray, and nothing here fits a 38-pin ESP32.
- ESP32 dev board, 38-pin ESP-WROOM-32
— a DevKitC-style board rather than a literal NodeMCU-32S, but it is the same
ESP-WROOM-32 module on the same pinout, and the config’s
board: nodemcu-32skey builds for it unchanged. Ships as a 2-pack. - BME280 module, 2-pack —
temperature, humidity and barometric pressure on I²C. The config expects it
at
0x76, which is the default for these purple boards. - TSL2561 module, 2-pack —
a real lux figure with IR compensation, instead of a photoresistor’s rough
percentage. The config expects
0x39, the float-address default. - SK6812 RGBW strip, 1 m —
the config drives four pixels, so cut four off the end. RGBW, not RGB: it
declares
type: GRBWand there is a real white LED in each pixel. - The same mini PIR as the ESP8266 build, and the same 5 V supply — this board takes power over its own micro-USB socket too.
| Function | GPIO | Notes |
|---|---|---|
| I²C SDA | 5 | BME280 and TSL2561 share the bus |
| I²C SCL | 4 | |
| PIR out | 18 | 300 ms delayed_on filter |
| SK6812 data | 19 | 4 pixels, method: ESP32_I2S_1 |
| Status LED | 2 | Onboard, same idea as the ESP8266 build |
On top of the raw readings it computes altitude from pressure and temperature, plus dew point and absolute humidity — all in lambdas you can read in the config rather than in a Home Assistant template.
Delete the two offsets before you trust a single reading. The shipped config
carries offset: -5.3 on temperature and offset: 29 on humidity. Those are one
particular sensor’s corrections, and +29% relative humidity is not a small nudge
— it is the difference between “dry” and “damp”. Comment both out, let the board
read raw for a day beside a reference you trust, then put your own numbers in.
i2c: has scan: true, so the boot log lists every address it found. That is
the first thing to read if a sensor is missing — a BME280 clone strapped to
0x77 shows up there immediately.
Troubleshooting
The temperature reads nan. Almost always the DHT22 pull-up. If you’re using
a bare four-pin sensor rather than the module, add a 10 kΩ resistor between data
and 3V3.
It won’t boot, or won’t flash, with the LED connected. D3 is GPIO0, the pin the ESP8266 samples at reset to decide whether to enter flash mode. If the board misbehaves, pull the blue leg off while you flash and put it back afterwards.
Motion triggers constantly. The PIR’s retrigger timer is turned up too far, or it is looking at a heat source — a vent, a radiator, direct sun on a wall. Turn sensitivity down and move it off the heat.
Luminance sits at 0 or 100. The calibrate_linear block is two points from
somebody else’s room. Turn the logger up, watch the raw value with the lights on
and then off, and replace the two points with what you actually see.
It drops off WiFi in a far room. The ESP8266’s antenna is not strong. Before adding a repeater, try rotating the node 90° — orientation is worth a surprising amount.
Files & downloads
Printable parts
ESPHome configuration s (2)
Copy this into your ESPHome directory and adjust the substitutions at the top.
Secrets are referenced by name — see secrets.yaml.example
.
bruh-multisensor.yaml
# BRUH Multisensor# ────────────────────────────────────────────────────────────# ESP8266 room sensor with temperature, humidity, motion# detection, ambient light level, and an RGB status LED.## Hardware:# - Board: NodeMCU v2 (ESP8266)# - Sensor: DHT22 on D7 (temperature + humidity)# - Sensor: Photoresistor on A0 (ambient light)# - Sensor: PIR motion on D5# - Output: RGB LED on D1 (red), D2 (green), D3 (blue)# ────────────────────────────────────────────────────────────
substitutions: device_name: "BRUH Multisensor" device_id: "bruh-multisensor"
esphome: name: ${device_id}
esp8266: board: nodemcuv2
wifi: ssid: !secret wifi_ssid password: !secret wifi_password
ota: password: !secret ota_password platform: esphome
api:
status_led: pin: number: GPIO2
logger: level: ERROR
binary_sensor: - platform: gpio pin: D5 name: "${device_name} Motion" device_class: motion - platform: status name: "${device_name} Status"
light: - platform: rgb name: "${device_name} LED" red: red_pwm green: green_pwm blue: blue_pwm effects: - flicker: - strobe: - random:
output: - platform: esp8266_pwm pin: D1 id: red_pwm - platform: esp8266_pwm pin: D2 id: green_pwm - platform: esp8266_pwm pin: D3 id: blue_pwm
sensor: - platform: dht pin: D7 model: DHT22 update_interval: 5s temperature: name: "${device_name} Temperature" filters: - lambda: return x * (9.0/5.0) + 32.0; unit_of_measurement: "°F" humidity: name: "${device_name} Humidity"
- platform: adc pin: A0 name: "${device_name} Luminance" update_interval: 2s unit_of_measurement: "%" accuracy_decimals: 0 filters: - calibrate_linear: - 0.0 -> 0.0 - 0.6 -> 100.0 - delta: 3.0
- platform: wifi_signal name: "${device_name} WiFi Signal" update_interval: 300s
switch: - platform: restart name: "${device_name} Restart" bruh-multisensor-esp32.yaml
# BRUH Multisensor ESP32# ────────────────────────────────────────────────────────────# ESP32 variant with BME280, TSL2561 ambient light,# PIR motion detection, and RGBW neopixel status LED.# Includes computed sensors: altitude, dew point,# absolute humidity, heat index.## Hardware:# - Board: NodeMCU-32S (ESP32)# - Sensor: BME280 on I2C (temperature, pressure, humidity)# - Sensor: TSL2561 on I2C (ambient light)# - Sensor: PIR motion on GPIO18# - Output: SK6812 RGBW LED (4 pixels) on GPIO19# ────────────────────────────────────────────────────────────
substitutions: device_name: "BRUH Multisensor 2" device_id: "bruh-multisensor-2" i2c_sda_pin: GPIO5 i2c_scl_pin: GPIO4 led_pin: GPIO19 pir_pin: GPIO18
esphome: name: ${device_id}
esp32: board: nodemcu-32s
wifi: ssid: !secret wifi_ssid password: !secret wifi_password
ota: password: !secret ota_password platform: esphome
api:
status_led: pin: number: GPIO2
logger:
sensor: - platform: bme280_i2c temperature: name: "${device_name} BME280 Temperature" oversampling: 16x filters: - offset: -5.3 id: bme280_temperature pressure: name: "${device_name} BME280 Pressure" oversampling: 16x id: bme280_pressure humidity: name: "${device_name} BME280 Humidity" oversampling: 16x filters: - offset: 29 id: bme280_humidity address: 0x76 update_interval: 30s
- platform: template name: "Altitude" unit_of_measurement: "ft" icon: mdi:signal accuracy_decimals: 0 update_interval: 15s lambda: |- const float STANDARD_SEA_LEVEL_PRESSURE = 1013.25f; float T = id(bme280_temperature).state; float P = id(bme280_pressure).state; if (isnan(T) || isnan(P) || P <= 0) return NAN; float alt_m = ((T + 273.15f) / 0.0065f) * (powf((STANDARD_SEA_LEVEL_PRESSURE / P), 0.190234f) - 1.0f); return alt_m * 3.28084f;
- platform: absolute_humidity name: "Absolute Humidity" temperature: bme280_temperature humidity: bme280_humidity accuracy_decimals: 2
- platform: template name: "Dew Point" unit_of_measurement: "°F" icon: mdi:thermometer-alert accuracy_decimals: 1 lambda: |- float t = id(bme280_temperature).state; float h = id(bme280_humidity).state; if (isnan(t) || isnan(h) || h <= 0) return NAN; if (h < 1.0f) h = 1.0f; if (h > 100.0f) h = 100.0f; const float a = 17.62f; const float b = 243.12f; float gamma = logf(h / 100.0f) + (a * t) / (b + t); float dew_c = (b * gamma) / (a - gamma); return dew_c * 9.0f / 5.0f + 32.0f;
- platform: tsl2561 name: "${device_name} TSL2561 Ambient Light" address: 0x39 update_interval: 60s
- platform: wifi_signal name: "${device_name} WiFi Signal" update_interval: 300s
binary_sensor: - platform: gpio name: "Motion" id: motion pin: ${pir_pin} device_class: motion filters: - delayed_on: 300ms - platform: status name: "${device_name} Status"
i2c: sda: ${i2c_sda_pin} scl: ${i2c_scl_pin} scan: true
light: - platform: neopixelbus variant: SK6812 pin: ${led_pin} num_leds: 4 type: GRBW name: "${device_name} LED" method: ESP32_I2S_1 effects: - flicker: - flicker: name: "Flicker BRUH" alpha: 92% intensity: 4% - strobe: - random: - addressable_rainbow: - addressable_scan: - addressable_color_wipe: - addressable_twinkle: - addressable_twinkle: name: "Twinkle BRUH" twinkle_probability: 80% progress_interval: 4ms - addressable_random_twinkle: - addressable_fireworks: - addressable_flicker: - addressable_flicker: name: "Addressable Flicker BRUH" update_interval: 25ms intensity: 15%
switch: - platform: restart name: "${device_name} Restart"