Skip to content

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.

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.

SettingValue
Layer height0.2 mm
Infill20%
SupportsNone
MaterialPLA or PETG
OrientationFlat, 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.

PartNodeMCU pinGPIONotes
DHT22 dataD713platform: dht, model: DHT22, every 5 s
PIR outD514binary_sensor with device_class: motion
Light module AOA0ADCAnalogue out, not the digital threshold pin
RGB LED redD15220 Ω in series
RGB LED greenD24220 Ω in series
RGB LED blueD30220 Ω in series — and see Troubleshooting
Status LED2The board’s own blue LED, claimed by status_led
Power3V3 and GNDThe 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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. Flash it (below), confirm the entities appear in Home Assistant, and only then put the lid on.

Flashing

  1. Copy bruh-multisensor.yaml from the download section below into your ESPHome folder.

  2. Fill in secrets.yamlwifi_ssid, wifi_password, ota_password, ap_password. There is a secrets.yaml.example in the repo with every key these configs use.

  3. 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"
  4. 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_measurement line to get °C.
  • Motion as a binary_sensor with device_class: motion, so it drives presence automations directly.
  • Luminance as a percentage, calibrated with a two-point calibrate_linear and a delta: 3 filter 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-32s key 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: GRBW and 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.
FunctionGPIONotes
I²C SDA5BME280 and TSL2561 share the bus
I²C SCL4
PIR out18300 ms delayed_on filter
SK6812 data194 pixels, method: ESP32_I2S_1
Status LED2Onboard, 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

Loading 3D model…
Drag to rotate · Scroll to zoom · Right-click to pan

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 102 lines
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"

View on GitHub · Download raw

bruh-multisensor-esp32.yaml 163 lines
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"

View on GitHub · Download raw

Every file for this project on GitHub → Also in the folder: arduino-legacy/ — legacy Arduino sketches, 2 files; fritzing/ — wiring diagrams, 2 files.